You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
931 lines
32 KiB
931 lines
32 KiB
"""
|
|
The MIT License (MIT)
|
|
|
|
Copyright (c) 2021-present Dolfies
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a
|
|
copy of this software and associated documentation files (the "Software"),
|
|
to deal in the Software without restriction, including without limitation
|
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
and/or sell copies of the Software, and to permit persons to whom the
|
|
Software is furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
DEALINGS IN THE SOFTWARE.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta
|
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
|
|
|
from .billing import PaymentSource
|
|
from .enums import (
|
|
PaymentGateway,
|
|
SubscriptionDiscountType,
|
|
SubscriptionInterval,
|
|
SubscriptionInvoiceStatus,
|
|
SubscriptionStatus,
|
|
SubscriptionType,
|
|
try_enum,
|
|
)
|
|
from .metadata import Metadata
|
|
from .mixins import Hashable
|
|
from .utils import MISSING, _get_as_snowflake, parse_time, snowflake_time, utcnow
|
|
|
|
if TYPE_CHECKING:
|
|
from typing_extensions import Self
|
|
|
|
from .abc import Snowflake
|
|
from .guild import Guild
|
|
from .state import ConnectionState
|
|
from .types.subscriptions import (
|
|
SubscriptionDiscount as SubscriptionDiscountPayload,
|
|
SubscriptionInvoice as SubscriptionInvoicePayload,
|
|
SubscriptionInvoiceItem as SubscriptionInvoiceItemPayload,
|
|
SubscriptionItem as SubscriptionItemPayload,
|
|
SubscriptionRenewalMutations as SubscriptionRenewalMutationsPayload,
|
|
PartialSubscription as PartialSubscriptionPayload,
|
|
Subscription as SubscriptionPayload,
|
|
SubscriptionTrial as SubscriptionTrialPayload,
|
|
)
|
|
|
|
__all__ = (
|
|
'SubscriptionItem',
|
|
'SubscriptionDiscount',
|
|
'SubscriptionInvoiceItem',
|
|
'SubscriptionInvoice',
|
|
'SubscriptionRenewalMutations',
|
|
'Subscription',
|
|
'SubscriptionTrial',
|
|
)
|
|
|
|
|
|
class SubscriptionItem(Hashable):
|
|
"""Represents a Discord subscription item.
|
|
|
|
.. container:: operations
|
|
|
|
.. describe:: x == y
|
|
|
|
Checks if two subscription items are equal.
|
|
|
|
.. describe:: x != y
|
|
|
|
Checks if two subscription items are not equal.
|
|
|
|
.. describe:: hash(x)
|
|
|
|
Returns the item's hash.
|
|
|
|
.. describe:: len(x)
|
|
|
|
Returns the quantity of the subscription item.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
id: Optional[:class:`int`]
|
|
The ID of the subscription item. Always available when received from the API.
|
|
quantity: :class:`int`
|
|
How many of the item have been/are being purchased.
|
|
plan_id: :class:`int`
|
|
The ID of the plan the item is for.
|
|
"""
|
|
|
|
__slots__ = ('id', 'quantity', 'plan_id')
|
|
|
|
def __init__(self, *, id: Optional[int] = None, plan_id: int, quantity: int = 1) -> None:
|
|
self.id: Optional[int] = id
|
|
self.quantity: int = quantity
|
|
self.plan_id: int = plan_id
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<SubscriptionItem {f"id={self.id} " if self.id else ""}plan_id={self.plan_id} quantity={self.quantity}>'
|
|
|
|
def __len__(self) -> int:
|
|
return self.quantity
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: SubscriptionItemPayload) -> Self:
|
|
return cls(id=int(data['id']), plan_id=int(data['plan_id']), quantity=int(data.get('quantity', 1)))
|
|
|
|
def to_dict(self, with_id: bool = True) -> dict:
|
|
data = {
|
|
'quantity': self.quantity,
|
|
'plan_id': self.plan_id,
|
|
}
|
|
if self.id and with_id:
|
|
data['id'] = self.id
|
|
|
|
return data
|
|
|
|
|
|
class SubscriptionDiscount:
|
|
"""Represents a discount on a Discord subscription item.
|
|
|
|
.. container:: operations
|
|
|
|
.. describe:: int(x)
|
|
|
|
Returns the discount's amount.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
type: :class:`SubscriptionDiscountType`
|
|
The type of the discount.
|
|
amount: :class:`int`
|
|
How much the discount is.
|
|
"""
|
|
|
|
__slots__ = ('type', 'amount')
|
|
|
|
def __init__(self, data: SubscriptionDiscountPayload) -> None:
|
|
self.type: SubscriptionDiscountType = try_enum(SubscriptionDiscountType, data['type'])
|
|
self.amount: int = data['amount']
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<SubscriptionDiscount type={self.type!r} amount={self.amount}>'
|
|
|
|
def __int__(self) -> int:
|
|
return self.amount
|
|
|
|
|
|
class SubscriptionInvoiceItem(Hashable):
|
|
"""Represents an invoice item.
|
|
|
|
.. container:: operations
|
|
|
|
.. describe:: x == y
|
|
|
|
Checks if two invoice items are equal.
|
|
|
|
.. describe:: x != y
|
|
|
|
Checks if two invoice items are not equal.
|
|
|
|
.. describe:: hash(x)
|
|
|
|
Returns the invoice's hash.
|
|
|
|
.. describe:: len(x)
|
|
|
|
Returns the quantity of the invoice item.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
id: :class:`int`
|
|
The ID of the invoice item.
|
|
quantity: :class:`int`
|
|
How many of the item have been/are being purchased.
|
|
amount: :class:`int`
|
|
The price of the item. This includes discounts.
|
|
proration: :class:`bool`
|
|
Whether the item is prorated.
|
|
plan_id: :class:`int`
|
|
The ID of the subscription plan the item represents.
|
|
plan_price: :class:`int`
|
|
The price of the subscription plan the item represents. This does not include discounts.
|
|
discounts: List[:class:`SubscriptionDiscount`]
|
|
A list of discounts applied to the item.
|
|
metadata: :class:`Metadata`
|
|
Extra metadata about the invoice item.
|
|
|
|
.. versionadded:: 2.1
|
|
"""
|
|
|
|
__slots__ = ('id', 'quantity', 'amount', 'proration', 'sku_id', 'plan_id', 'plan_price', 'discounts', 'metadata')
|
|
|
|
def __init__(self, data: SubscriptionInvoiceItemPayload) -> None:
|
|
self.id: int = int(data['id'])
|
|
self.quantity: int = data['quantity']
|
|
self.amount: int = data['amount']
|
|
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_price: int = data['subscription_plan_price']
|
|
self.discounts: List[SubscriptionDiscount] = [SubscriptionDiscount(d) for d in data['discounts']]
|
|
self.metadata: Metadata = Metadata(data.get('tenant_metadata', {}))
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<SubscriptionInvoiceItem id={self.id} quantity={self.quantity} amount={self.amount}>'
|
|
|
|
def __len__(self) -> int:
|
|
return self.quantity
|
|
|
|
@property
|
|
def savings(self) -> int:
|
|
""":class:`int`: The total amount of discounts on the invoice item."""
|
|
return self.plan_price - self.amount
|
|
|
|
def is_discounted(self) -> bool:
|
|
""":class:`bool`: Indicates if the invoice item has a discount."""
|
|
return bool(self.discounts)
|
|
|
|
def is_trial(self) -> bool:
|
|
""":class:`bool`: Indicates if the invoice item is a trial."""
|
|
return not self.amount or any(discount.type is SubscriptionDiscountType.premium_trial for discount in self.discounts)
|
|
|
|
|
|
class SubscriptionInvoice(Hashable):
|
|
"""Represents an invoice for a Discord subscription.
|
|
|
|
.. container:: operations
|
|
|
|
.. describe:: x == y
|
|
|
|
Checks if two invoices are equal.
|
|
|
|
.. describe:: x != y
|
|
|
|
Checks if two invoices are not equal.
|
|
|
|
.. describe:: hash(x)
|
|
|
|
Returns the invoice's hash.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
subscription: Optional[:class:`Subscription`]
|
|
The subscription the invoice is for. Not available for new subscription previews.
|
|
id: :class:`int`
|
|
The ID of the invoice.
|
|
status: Optional[:class:`SubscriptionInvoiceStatus`]
|
|
The status of the invoice. Not available for subscription previews.
|
|
currency: :class:`str`
|
|
The currency the invoice is in.
|
|
subtotal: :class:`int`
|
|
The subtotal of the invoice.
|
|
tax: :class:`int`
|
|
The tax applied to the invoice.
|
|
total: :class:`int`
|
|
The total of the invoice.
|
|
tax_inclusive: :class:`bool`
|
|
Whether the subtotal is inclusive of all taxes.
|
|
items: List[:class:`SubscriptionInvoiceItem`]
|
|
The items in the invoice.
|
|
current_period_start: :class:`datetime.datetime`
|
|
When the current billing period started.
|
|
current_period_end: :class:`datetime.datetime`
|
|
When the current billing period ends.
|
|
"""
|
|
|
|
__slots__ = (
|
|
'_state',
|
|
'subscription',
|
|
'id',
|
|
'status',
|
|
'currency',
|
|
'subtotal',
|
|
'tax',
|
|
'total',
|
|
'tax_inclusive',
|
|
'items',
|
|
'current_period_start',
|
|
'current_period_end',
|
|
)
|
|
|
|
def __init__(
|
|
self, subscription: Optional[Subscription], *, data: SubscriptionInvoicePayload, state: ConnectionState
|
|
) -> None:
|
|
self._state = state
|
|
self.subscription = subscription
|
|
self._update(data)
|
|
|
|
def _update(self, data: SubscriptionInvoicePayload) -> None:
|
|
self.id: int = int(data['id'])
|
|
self.status: Optional[SubscriptionInvoiceStatus] = (
|
|
try_enum(SubscriptionInvoiceStatus, data['status']) if 'status' in data else None
|
|
)
|
|
self.currency: str = data['currency']
|
|
self.subtotal: int = data['subtotal']
|
|
self.tax: int = data.get('tax', 0)
|
|
self.total: int = data['total']
|
|
self.tax_inclusive: bool = data['tax_inclusive']
|
|
self.items: List[SubscriptionInvoiceItem] = [SubscriptionInvoiceItem(d) for d in data.get('invoice_items', [])]
|
|
|
|
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
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<SubscriptionInvoice id={self.id} status={self.status!r} total={self.total}>'
|
|
|
|
def is_discounted(self) -> bool:
|
|
""":class:`bool`: Indicates if the invoice has a discount."""
|
|
return any(item.discounts for item in self.items)
|
|
|
|
def is_preview(self) -> bool:
|
|
""":class:`bool`: Indicates if the invoice is a preview and not real."""
|
|
return self.subscription is None or self.status is None
|
|
|
|
async def pay(
|
|
self,
|
|
payment_source: Optional[Snowflake] = None,
|
|
currency: str = 'usd',
|
|
*,
|
|
payment_source_token: Optional[str] = None,
|
|
return_url: Optional[str] = None,
|
|
) -> None:
|
|
"""|coro|
|
|
|
|
Pays the invoice.
|
|
|
|
Parameters
|
|
----------
|
|
payment_source: Optional[:class:`PaymentSource`]
|
|
The payment source the invoice should be paid with.
|
|
currency: :class:`str`
|
|
The currency to pay with.
|
|
payment_source_token: Optional[:class:`str`]
|
|
The token used to authorize with the payment source.
|
|
return_url: Optional[:class:`str`]
|
|
The URL to return to after the payment is complete.
|
|
|
|
Raises
|
|
------
|
|
TypeError
|
|
The invoice is a preview and not real.
|
|
NotFound
|
|
The invoice is not open or found.
|
|
HTTPException
|
|
Paying the invoice failed.
|
|
"""
|
|
if self.is_preview() or not self.subscription:
|
|
raise TypeError('Cannot pay a nonexistant invoice')
|
|
|
|
data = await self._state.http.pay_invoice(
|
|
self.subscription.id,
|
|
self.id,
|
|
payment_source.id if payment_source else None,
|
|
payment_source_token,
|
|
currency,
|
|
return_url,
|
|
)
|
|
self.subscription._update(data)
|
|
|
|
|
|
class SubscriptionRenewalMutations:
|
|
"""Represents a subscription renewal mutation.
|
|
|
|
This represents changes to a subscription that will occur after renewal.
|
|
|
|
.. container:: operations
|
|
|
|
.. describe:: len(x)
|
|
|
|
Returns the number of items in the changed subscription, including quantity.
|
|
|
|
.. describe:: bool(x)
|
|
|
|
Returns whether any mutations are present.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
payment_gateway_plan_id: Optional[:class:`str`]
|
|
The payment gateway's new plan ID for the subscription.
|
|
This signifies an external plan change.
|
|
items: Optional[List[:class:`SubscriptionItem`]]
|
|
The new items of the subscription.
|
|
"""
|
|
|
|
__slots__ = ('payment_gateway_plan_id', 'items')
|
|
|
|
def __init__(self, data: SubscriptionRenewalMutationsPayload) -> None:
|
|
self.payment_gateway_plan_id: Optional[str] = data.get('payment_gateway_plan_id')
|
|
self.items: Optional[List[SubscriptionItem]] = (
|
|
[SubscriptionItem.from_dict(item) for item in data['items']] if 'items' in data else None
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return (
|
|
f'<SubscriptionRenewalMutations payment_gateway_plan_id={self.payment_gateway_plan_id!r} items={self.items!r}>'
|
|
)
|
|
|
|
def __len__(self) -> int:
|
|
return sum(item.quantity for item in self.items) if self.items else 0
|
|
|
|
def __bool__(self) -> bool:
|
|
return self.is_mutated()
|
|
|
|
def is_mutated(self) -> bool:
|
|
""":class:`bool`: Checks if any renewal mutations exist."""
|
|
return self.payment_gateway_plan_id is not None or self.items is not None
|
|
|
|
|
|
class Subscription(Hashable):
|
|
"""Represents a Discord subscription.
|
|
|
|
.. container:: operations
|
|
|
|
.. describe:: x == y
|
|
|
|
Checks if two premium subscriptions are equal.
|
|
|
|
.. describe:: x != y
|
|
|
|
Checks if two premium subscriptions are not equal.
|
|
|
|
.. describe:: hash(x)
|
|
|
|
Returns the subscription's hash.
|
|
|
|
.. describe:: len(x)
|
|
|
|
Returns the number of items in the subscription, including quantity.
|
|
|
|
.. describe:: bool(x)
|
|
|
|
Checks if the subscription is currently active and offering perks.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
id: :class:`int`
|
|
The ID of the subscription.
|
|
type: :class:`SubscriptionType`
|
|
The type of the subscription.
|
|
status: Optional[:class:`SubscriptionStatus`]
|
|
The status of the subscription. This is ``None`` for fake subscriptions.
|
|
payment_gateway: Optional[:class:`PaymentGateway`]
|
|
The payment gateway used to bill the subscription.
|
|
country_code: Optional[:class:`str`]
|
|
The country code the subscription is billed in, if applicable.
|
|
|
|
.. versionadded:: 2.1
|
|
currency: :class:`str`
|
|
The currency the subscription is billed in.
|
|
items: List[:class:`SubscriptionItem`]
|
|
The items in the subscription.
|
|
renewal_mutations: :class:`SubscriptionRenewalMutations`
|
|
The mutations to the subscription that will occur after renewal.
|
|
trial_id: Optional[:class:`int`]
|
|
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`]
|
|
The ID of the payment source the subscription is paid with, if applicable.
|
|
payment_gateway_plan_id: Optional[:class:`str`]
|
|
The payment gateway's plan ID for the subscription, if applicable.
|
|
payment_gateway_subscription_id: Optional[:class:`str`]
|
|
The payment gateway's subscription ID for the subscription, if applicable.
|
|
price: Optional[:class:`int`]
|
|
The price of the subscription.
|
|
This is only available for certain third-party subscriptions.
|
|
|
|
.. versionadded:: 2.1
|
|
created_at: :class:`datetime.datetime`
|
|
When the subscription was created.
|
|
canceled_at: Optional[:class:`datetime.datetime`]
|
|
When the subscription was canceled.
|
|
This is only available for subscriptions with a :attr:`status` of :attr:`SubscriptionStatus.canceled`.
|
|
current_period_start: :class:`datetime.datetime`
|
|
When the current billing period started.
|
|
current_period_end: :class:`datetime.datetime`
|
|
When the current billing period ends.
|
|
trial_ends_at: Optional[:class:`datetime.datetime`]
|
|
When the trial ends, if applicable.
|
|
streak_started_at: Optional[:class:`datetime.datetime`]
|
|
When the current subscription streak started.
|
|
use_storekit_resubscribe: :class:`bool`
|
|
Whether the subscription should be managed through StoreKit.
|
|
|
|
.. versionadded:: 2.1
|
|
ended_at: Optional[:class:`datetime.datetime`]
|
|
When the subscription finally ended.
|
|
metadata: :class:`Metadata`
|
|
Extra metadata about the subscription.
|
|
latest_invoice: Optional[:class:`SubscriptionInvoice`]
|
|
The latest invoice for the subscription, if applicable.
|
|
"""
|
|
|
|
__slots__ = (
|
|
'_state',
|
|
'id',
|
|
'type',
|
|
'status',
|
|
'payment_gateway',
|
|
'country_code',
|
|
'currency',
|
|
'items',
|
|
'renewal_mutations',
|
|
'trial_id',
|
|
'payment_source_id',
|
|
'payment_gateway_plan_id',
|
|
'payment_gateway_subscription_id',
|
|
'price',
|
|
'created_at',
|
|
'canceled_at',
|
|
'current_period_start',
|
|
'current_period_end',
|
|
'trial_ends_at',
|
|
'streak_started_at',
|
|
'use_storekit_resubscribe',
|
|
'metadata',
|
|
'ended_at',
|
|
'discount_id',
|
|
'discount_expires_at',
|
|
'latest_invoice',
|
|
)
|
|
|
|
def __init__(self, *, data: Union[PartialSubscriptionPayload, SubscriptionPayload], state: ConnectionState) -> None:
|
|
self._state = state
|
|
self._update(data)
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<Subscription id={self.id} currency={self.currency!r} items={self.items!r}>'
|
|
|
|
def __len__(self) -> int:
|
|
return sum(item.quantity for item in self.items)
|
|
|
|
def __bool__(self) -> bool:
|
|
return self.is_active()
|
|
|
|
def _update(self, data: Union[PartialSubscriptionPayload, SubscriptionPayload]) -> None:
|
|
self.id: int = int(data['id'])
|
|
self.type: SubscriptionType = try_enum(SubscriptionType, data['type'])
|
|
self.status: Optional[SubscriptionStatus] = (
|
|
try_enum(SubscriptionStatus, data['status']) if 'status' in data else None # type: ignore # ???
|
|
)
|
|
self.payment_gateway: Optional[PaymentGateway] = (
|
|
try_enum(PaymentGateway, data['payment_gateway']) if 'payment_gateway' in data else None
|
|
)
|
|
self.country_code: Optional[str] = data.get('country_code')
|
|
self.currency: str = data.get('currency', 'usd')
|
|
self.items: List[SubscriptionItem] = [SubscriptionItem.from_dict(item) for item in data.get('items', [])]
|
|
self.renewal_mutations: SubscriptionRenewalMutations = SubscriptionRenewalMutations(
|
|
data.get('renewal_mutations') or {}
|
|
)
|
|
|
|
self.trial_id: Optional[int] = _get_as_snowflake(data, 'trial_id')
|
|
self.payment_source_id: Optional[int] = _get_as_snowflake(data, 'payment_source_id')
|
|
self.payment_gateway_plan_id: Optional[str] = data.get('payment_gateway_plan_id')
|
|
self.payment_gateway_subscription_id: Optional[str] = data.get('payment_gateway_subscription_id')
|
|
self.price: Optional[int] = data.get('price')
|
|
|
|
self.created_at: datetime = parse_time(data.get('created_at')) or snowflake_time(self.id)
|
|
self.canceled_at: Optional[datetime] = parse_time(data.get('canceled_at'))
|
|
|
|
self.current_period_start: datetime = parse_time(data['current_period_start'])
|
|
self.current_period_end: datetime = parse_time(data['current_period_end'])
|
|
self.trial_ends_at: Optional[datetime] = parse_time(data.get('trial_ends_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)
|
|
|
|
# Some metadata is exposed; I don't love this implementation
|
|
metadata = data.get('metadata') or {}
|
|
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] = (
|
|
SubscriptionInvoice(self, data=data['latest_invoice'], state=self._state) if 'latest_invoice' in data else None # type: ignore # ???
|
|
)
|
|
|
|
@property
|
|
def cancelled_at(self) -> Optional[datetime]:
|
|
"""Optional[:class:`datetime.datetime`]: When the subscription was canceled.
|
|
This is only available for subscriptions with a :attr:`status` of :attr:`SubscriptionStatus.canceled`.
|
|
|
|
This is an alias of :attr:`canceled_at`.
|
|
"""
|
|
return self.canceled_at
|
|
|
|
@property
|
|
def guild(self) -> Optional[Guild]:
|
|
"""Optional[:class:`Guild`]: The guild the subscription's entitlements apply to, if applicable."""
|
|
# I think guild_id is deprecated
|
|
return self._state._get_guild(self.metadata.guild_id or self.metadata.application_subscription_guild_id)
|
|
|
|
@property
|
|
def grace_period(self) -> Optional[timedelta]:
|
|
"""Optional[:class:`datetime.timedelta`]: How many days past the renewal date the user has available to pay outstanding invoices.
|
|
|
|
.. note::
|
|
|
|
This is only available for past-due subscriptions.
|
|
|
|
.. versionchanged:: 2.1
|
|
|
|
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
|
|
"""
|
|
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
|
|
def remaining(self) -> timedelta:
|
|
""":class:`datetime.timedelta`: The remaining time until the subscription ends."""
|
|
if self.status in (SubscriptionStatus.active, SubscriptionStatus.cancelled):
|
|
return self.current_period_end - utcnow()
|
|
elif self.status == SubscriptionStatus.past_due:
|
|
# Grace expiry is now always provided
|
|
return self.grace_period_expires_at - utcnow() # type: ignore
|
|
elif self.status == SubscriptionStatus.account_hold:
|
|
# Max hold time is 30 days
|
|
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()
|
|
|
|
@property
|
|
def trial_remaining(self) -> timedelta:
|
|
""":class:`datetime.timedelta`: The remaining time until the trial applied to the subscription ends."""
|
|
if not self.trial_id:
|
|
return timedelta()
|
|
if not self.trial_ends_at:
|
|
# Infinite trial?
|
|
return self.remaining
|
|
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:
|
|
""":class:`bool`: Indicates if the subscription is currently active and providing perks."""
|
|
return self.remaining > timedelta()
|
|
|
|
def is_trial(self) -> bool:
|
|
""":class:`bool`: Indicates if the subscription is a trial."""
|
|
return self.trial_id is not None
|
|
|
|
async def edit(
|
|
self,
|
|
items: List[SubscriptionItem] = MISSING,
|
|
payment_source: Snowflake = MISSING,
|
|
currency: str = MISSING,
|
|
*,
|
|
status: SubscriptionStatus = MISSING,
|
|
payment_source_token: Optional[str] = None,
|
|
) -> None:
|
|
"""|coro|
|
|
|
|
Edits the subscription.
|
|
|
|
All parameters are optional.
|
|
|
|
Parameters
|
|
----------
|
|
items: List[:class:`SubscriptionItem`]
|
|
The new subscription items to use.
|
|
payment_source: :class:`int`
|
|
The new payment source for payment.
|
|
currency: :class:`str`
|
|
The new currency to use for payment.
|
|
status: :class:`SubscriptionStatus`
|
|
The new status of the subscription.
|
|
payment_source_token: Optional[:class:`str`]
|
|
The token used to authorize with the payment source.
|
|
|
|
Raises
|
|
------
|
|
Forbidden
|
|
You do not have permissions to edit the subscription.
|
|
HTTPException
|
|
Editing the subscription failed.
|
|
"""
|
|
payload = {}
|
|
if items is not MISSING:
|
|
payload['items'] = [item.to_dict() for item in items] if items else []
|
|
if payment_source is not MISSING:
|
|
payload['payment_source_id'] = payment_source.id
|
|
payload['payment_source_token'] = payment_source_token
|
|
if currency is not MISSING:
|
|
payload['currency'] = currency
|
|
if status is not MISSING:
|
|
payload['status'] = int(status)
|
|
|
|
data = await self._state.http.edit_subscription(self.id, **payload)
|
|
self._update(data)
|
|
|
|
async def delete(self) -> None:
|
|
"""|coro|
|
|
|
|
Deletes the subscription.
|
|
|
|
There is an alias of this called :meth:`cancel`.
|
|
|
|
Raises
|
|
------
|
|
HTTPException
|
|
Deleting the subscription failed.
|
|
"""
|
|
await self._state.http.delete_subscription(self.id)
|
|
|
|
async def cancel(self) -> None:
|
|
"""|coro|
|
|
|
|
Deletes the subscription.
|
|
|
|
Alias of :meth:`delete`.
|
|
|
|
Raises
|
|
------
|
|
HTTPException
|
|
Deleting the subscription failed.
|
|
"""
|
|
await self.delete()
|
|
|
|
async def preview_invoice(
|
|
self,
|
|
*,
|
|
items: List[SubscriptionItem] = MISSING,
|
|
payment_source: Snowflake = MISSING,
|
|
currency: str = MISSING,
|
|
apply_entitlements: bool = MISSING,
|
|
renewal: bool = MISSING,
|
|
discount_offer: Snowflake = MISSING,
|
|
) -> SubscriptionInvoice:
|
|
"""|coro|
|
|
|
|
Preview an invoice for the subscription with the given parameters.
|
|
|
|
All parameters are optional and default to the current subscription values.
|
|
|
|
Parameters
|
|
----------
|
|
items: List[:class:`SubscriptionItem`]
|
|
The items the previewed invoice should have.
|
|
payment_source: :class:`.PaymentSource`
|
|
The payment source the previewed invoice should be paid with.
|
|
currency: :class:`str`
|
|
The currency the previewed invoice should be paid in.
|
|
apply_entitlements: :class:`bool`
|
|
Whether to apply entitlements (credits) to the previewed invoice.
|
|
renewal: :class:`bool`
|
|
Whether the previewed invoice should be a renewal.
|
|
discount_offer: :class:`.DiscountOffer`
|
|
The discount offer to apply to the previewed invoice.
|
|
|
|
Raises
|
|
------
|
|
HTTPException
|
|
Failed to preview the invoice.
|
|
|
|
Returns
|
|
-------
|
|
:class:`SubscriptionInvoice`
|
|
The previewed invoice.
|
|
"""
|
|
payload: Dict[str, Any] = {}
|
|
if items is not MISSING:
|
|
payload['items'] = [item.to_dict() for item in items] if items else []
|
|
if payment_source:
|
|
payload['payment_source_id'] = payment_source.id
|
|
if currency:
|
|
payload['currency'] = currency
|
|
if apply_entitlements is not MISSING:
|
|
payload['apply_entitlements'] = apply_entitlements
|
|
if renewal is not MISSING:
|
|
payload['renewal'] = renewal
|
|
if discount_offer is not MISSING:
|
|
payload['user_discount_offer_id'] = discount_offer.id
|
|
|
|
if payload:
|
|
data = await self._state.http.preview_subscription_update(self.id, **payload)
|
|
else:
|
|
data = await self._state.http.get_subscription_preview(self.id)
|
|
|
|
return SubscriptionInvoice(self, data=data, state=self._state)
|
|
|
|
async def payment_source(self) -> Optional[PaymentSource]:
|
|
"""|coro|
|
|
|
|
Retrieves the payment source the subscription is paid with, if applicable.
|
|
|
|
Raises
|
|
------
|
|
NotFound
|
|
The payment source could not be found.
|
|
HTTPException
|
|
Retrieving the payment source failed.
|
|
|
|
Returns
|
|
-------
|
|
Optional[:class:`PaymentSource`]
|
|
The payment source the subscription is paid with, if applicable.
|
|
"""
|
|
if not self.payment_source_id:
|
|
return
|
|
|
|
data = await self._state.http.get_payment_source(self.payment_source_id)
|
|
return PaymentSource(data=data, state=self._state)
|
|
|
|
async def invoices(self):
|
|
"""|coro|
|
|
|
|
Retrieves all invoices for the subscription.
|
|
|
|
Raises
|
|
------
|
|
NotFound
|
|
The payment source or invoices could not be found.
|
|
HTTPException
|
|
Retrieving the invoices failed.
|
|
|
|
Returns
|
|
-------
|
|
List[:class:`SubscriptionInvoice`]
|
|
The invoices.
|
|
"""
|
|
state = self._state
|
|
data = await state.http.get_subscription_invoices(self.id)
|
|
return [SubscriptionInvoice(self, data=d, state=state) for d in data]
|
|
|
|
|
|
class SubscriptionTrial(Hashable):
|
|
"""Represents a subscription trial.
|
|
|
|
.. container:: operations
|
|
|
|
.. describe:: x == y
|
|
|
|
Checks if two trials are equal.
|
|
|
|
.. describe:: x != y
|
|
|
|
Checks if two trials are not equal.
|
|
|
|
.. describe:: hash(x)
|
|
|
|
Returns the trial's hash.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
id: :class:`int`
|
|
The ID of the trial.
|
|
interval: :class:`SubscriptionInterval`
|
|
The interval of the trial.
|
|
interval_count: :class:`int`
|
|
How many counts of the interval the trial provides.
|
|
"""
|
|
|
|
__slots__ = ('id', 'interval', 'interval_count', 'sku_id')
|
|
|
|
_INTERVAL_TABLE = {
|
|
SubscriptionInterval.day: 1,
|
|
SubscriptionInterval.month: 30,
|
|
SubscriptionInterval.year: 365,
|
|
}
|
|
|
|
def __init__(self, data: SubscriptionTrialPayload):
|
|
self.id: int = int(data['id'])
|
|
self.interval: SubscriptionInterval = try_enum(SubscriptionInterval, data['interval'])
|
|
self.interval_count: int = data['interval_count']
|
|
self.sku_id: int = int(data['sku_id'])
|
|
|
|
def __repr__(self) -> str:
|
|
return (
|
|
f'<SubscriptionTrial id={self.id} interval={self.interval} '
|
|
f'interval_count={self.interval_count} sku_id={self.sku_id}>'
|
|
)
|
|
|
|
@property
|
|
def duration(self) -> timedelta:
|
|
""":class:`datetime.timedelta`: How long the trial lasts."""
|
|
return timedelta(days=self.interval_count * self._INTERVAL_TABLE[self.interval])
|
|
|