import warnings
from copy import deepcopy

import stripe
from django.db import models
from django.utils import timezone
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _
from stripe.error import InvalidRequestError

from .. import enums
from .. import settings as djstripe_settings
from ..fields import (
    JSONField,
    StripeCurrencyCodeField,
    StripeDateTimeField,
    StripeDecimalCurrencyAmountField,
    StripeEnumField,
    StripeIdField,
    StripePercentField,
    StripeQuantumCurrencyAmountField,
)
from ..managers import SubscriptionManager
from ..utils import QuerySetMock, get_friendly_currency_amount
from .base import StripeModel


class DjstripeInvoiceTotalTaxAmount(models.Model):
    """
    An internal model that holds the value of elements of Invoice.total_tax_amounts

    Note that this is named with the prefix Djstripe to avoid potential
    collision with a Stripe API object name.
    """

    invoice = models.ForeignKey(
        "Invoice", on_delete=models.CASCADE, related_name="total_tax_amounts"
    )

    amount = StripeQuantumCurrencyAmountField(
        help_text="The amount, in cents, of the tax."
    )
    inclusive = models.BooleanField(
        help_text="Whether this tax amount is inclusive or exclusive."
    )
    tax_rate = models.ForeignKey(
        "TaxRate",
        on_delete=models.CASCADE,
        help_text="The tax rate that was applied to get this tax amount.",
    )

    class Meta:
        unique_together = ["invoice", "tax_rate"]


class DjstripeUpcomingInvoiceTotalTaxAmount(models.Model):
    """
    As per DjstripeInvoiceTotalTaxAmount, except for UpcomingInvoice
    """

    invoice = models.ForeignKey(
        # Don't define related_name since property is defined in UpcomingInvoice
        "UpcomingInvoice",
        on_delete=models.CASCADE,
        related_name="+",
    )

    amount = StripeQuantumCurrencyAmountField(
        help_text="The amount, in cents, of the tax."
    )
    inclusive = models.BooleanField(
        help_text="Whether this tax amount is inclusive or exclusive."
    )
    tax_rate = models.ForeignKey(
        "TaxRate",
        on_delete=models.CASCADE,
        help_text="The tax rate that was applied to get this tax amount.",
    )

    class Meta:
        unique_together = ["invoice", "tax_rate"]


class Coupon(StripeModel):
    id = StripeIdField(max_length=500)
    amount_off = StripeDecimalCurrencyAmountField(
        null=True,
        blank=True,
        help_text="Amount (as decimal) that will be taken off the subtotal of any "
        "invoices for this customer.",
    )
    currency = StripeCurrencyCodeField(null=True, blank=True)
    duration = StripeEnumField(
        enum=enums.CouponDuration,
        help_text=(
            "Describes how long a customer who applies this coupon "
            "will get the discount."
        ),
    )
    duration_in_months = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="If `duration` is `repeating`, the number of months "
        "the coupon applies.",
    )
    max_redemptions = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Maximum number of times this coupon can be redeemed, in total, "
        "before it is no longer valid.",
    )
    name = models.TextField(
        max_length=5000,
        default="",
        blank=True,
        help_text=(
            "Name of the coupon displayed to customers on for instance invoices "
            "or receipts."
        ),
    )
    percent_off = StripePercentField(
        null=True,
        blank=True,
        help_text=(
            "Percent that will be taken off the subtotal of any invoices for "
            "this customer for the duration of the coupon. "
            "For example, a coupon with percent_off of 50 will make a "
            "$100 invoice $50 instead."
        ),
    )
    redeem_by = StripeDateTimeField(
        null=True,
        blank=True,
        help_text="Date after which the coupon can no longer be redeemed. "
        "Max 5 years in the future.",
    )
    times_redeemed = models.PositiveIntegerField(
        editable=False,
        default=0,
        help_text="Number of times this coupon has been applied to a customer.",
    )
    # valid = models.BooleanField(editable=False)

    # XXX
    DURATION_FOREVER = "forever"
    DURATION_ONCE = "once"
    DURATION_REPEATING = "repeating"

    class Meta:
        unique_together = ("id", "livemode")

    stripe_class = stripe.Coupon
    stripe_dashboard_item_name = "coupons"

    def __str__(self):
        if self.name:
            return self.name
        return self.human_readable

    @property
    def human_readable_amount(self):
        if self.percent_off:
            amount = "{percent_off}%".format(percent_off=self.percent_off)
        else:
            amount = get_friendly_currency_amount(self.amount_off or 0, self.currency)
        return "{amount} off".format(amount=amount)

    @property
    def human_readable(self):
        if self.duration == self.DURATION_REPEATING:
            if self.duration_in_months == 1:
                duration = "for {duration_in_months} month"
            else:
                duration = "for {duration_in_months} months"
            duration = duration.format(duration_in_months=self.duration_in_months)
        else:
            duration = self.duration
        return "{amount} {duration}".format(
            amount=self.human_readable_amount, duration=duration
        )


class BaseInvoice(StripeModel):
    """
    The abstract base model shared by Invoice and UpcomingInvoice

    Note:
    Most fields are defined on BaseInvoice so they're available to both models.
    ManyToManyFields are an exception, since UpcomingInvoice doesn't exist in the db.
    """

    stripe_class = stripe.Invoice
    stripe_dashboard_item_name = "invoices"

    account_country = models.CharField(
        max_length=2,
        default="",
        blank=True,
        help_text="The country of the business associated with this invoice, "
        "most often the business creating the invoice.",
    )
    account_name = models.TextField(
        max_length=5000,
        blank=True,
        help_text="The public name of the business associated with this invoice, "
        "most often the business creating the invoice.",
    )
    amount_due = StripeDecimalCurrencyAmountField(
        help_text="Final amount due (as decimal) at this time for this invoice. "
        "If the invoice's total is smaller than the minimum charge amount, "
        "for example, or if there is account credit that can be applied to the "
        "invoice, the amount_due may be 0. If there is a positive starting_balance "
        "for the invoice (the customer owes money), the amount_due will also take that "
        "into account. The charge that gets generated for the invoice will be for "
        "the amount specified in amount_due."
    )
    amount_paid = StripeDecimalCurrencyAmountField(
        null=True,  # XXX: This is not nullable, but it's a new field
        help_text="The amount, (as decimal), that was paid.",
    )
    amount_remaining = StripeDecimalCurrencyAmountField(
        null=True,  # XXX: This is not nullable, but it's a new field
        help_text="The amount remaining, (as decimal), that is due.",
    )
    application_fee_amount = StripeDecimalCurrencyAmountField(
        null=True,
        blank=True,
        help_text="The fee (as decimal) that will be applied to the invoice and "
        "transferred to the application owner's "
        "Stripe account when the invoice is paid.",
    )
    attempt_count = models.IntegerField(
        help_text="Number of payment attempts made for this invoice, "
        "from the perspective of the payment retry schedule. "
        "Any payment attempt counts as the first attempt, and subsequently "
        "only automatic retries increment the attempt count. "
        "In other words, manual payment attempts after the first attempt do not affect "
        "the retry schedule."
    )
    attempted = models.BooleanField(
        default=False,
        help_text="Whether or not an attempt has been made to pay the invoice. "
        "An invoice is not attempted until 1 hour after the ``invoice.created`` "
        "webhook, for example, so you might not want to display that invoice as "
        "unpaid to your users.",
    )
    auto_advance = models.NullBooleanField(
        help_text="Controls whether Stripe will perform automatic collection of the "
        "invoice. When false, the invoice’s state will not automatically "
        "advance without an explicit action."
    )
    billing_reason = StripeEnumField(
        default="",
        blank=True,
        enum=enums.InvoiceBillingReason,
        help_text="Indicates the reason why the invoice was created. "
        "subscription_cycle indicates an invoice created by a subscription advancing "
        "into a new period. subscription_create indicates an invoice created due to "
        "creating a subscription. subscription_update indicates an invoice created due "
        "to updating a subscription. subscription is set for all old invoices to "
        "indicate either a change to a subscription or a period advancement. "
        "manual is set for all invoices unrelated to a subscription "
        "(for example: created via the invoice editor). The upcoming value is "
        "reserved for simulated invoices per the upcoming invoice endpoint. "
        "subscription_threshold indicates an invoice created due to a billing "
        "threshold being reached.",
    )
    charge = models.OneToOneField(
        "Charge",
        on_delete=models.CASCADE,
        null=True,
        # we need to use the %(class)s placeholder to avoid related name
        # clashes between Invoice and UpcomingInvoice
        related_name="latest_%(class)s",
        help_text="The latest charge generated for this invoice, if any.",
    )
    # deprecated, will be removed in 2.3
    closed = models.NullBooleanField(
        default=False,
        help_text="Whether or not the invoice is still trying to collect payment."
        " An invoice is closed if it's either paid or it has been marked closed. "
        "A closed invoice will no longer attempt to collect payment.",
    )
    collection_method = StripeEnumField(
        enum=enums.InvoiceBilling,
        null=True,
        help_text=(
            "When charging automatically, Stripe will attempt to pay this invoice "
            "using the default source attached to the customer. "
            "When sending an invoice, Stripe will email this invoice to the customer "
            "with payment instructions."
        ),
    )
    currency = StripeCurrencyCodeField()
    customer = models.ForeignKey(
        "Customer",
        on_delete=models.CASCADE,
        # we need to use the %(class)s placeholder to avoid related name
        # clashes between Invoice and UpcomingInvoice
        related_name="%(class)ss",
        help_text="The customer associated with this invoice.",
    )
    customer_address = JSONField(
        null=True,
        blank=True,
        help_text="The customer’s address. Until the invoice is finalized, this "
        "field will equal customer.address. Once the invoice is finalized, this field "
        "will no longer be updated.",
    )
    customer_email = models.TextField(
        max_length=5000,
        blank=True,
        help_text="The customer’s email. Until the invoice is finalized, this field "
        "will equal customer.email. Once the invoice is finalized, this field will no "
        "longer be updated.",
    )
    customer_name = models.TextField(
        max_length=5000,
        blank=True,
        help_text="The customer’s name. Until the invoice is finalized, this field "
        "will equal customer.name. Once the invoice is finalized, this field will no "
        "longer be updated.",
    )
    customer_phone = models.TextField(
        max_length=5000,
        blank=True,
        help_text="The customer’s phone number. Until the invoice is finalized, "
        "this field will equal customer.phone_. Once the invoice is finalized, "
        "this field will no longer be updated.",
    )
    customer_shipping = JSONField(
        null=True,
        blank=True,
        help_text="The customer’s shipping information. Until the invoice is "
        "finalized, this field will equal customer.shipping. Once the invoice is "
        "finalized, this field will no longer be updated.",
    )
    customer_tax_exempt = StripeEnumField(
        enum=enums.CustomerTaxExempt,
        default="",
        help_text="The customer’s tax exempt status. Until the invoice is finalized, "
        "this field will equal customer.tax_exempt. Once the invoice is "
        "finalized, this field will no longer be updated.",
    )
    default_payment_method = models.ForeignKey(
        "PaymentMethod",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
        help_text="Default payment method for the invoice. It must belong to the "
        "customer associated with the invoice. If not set, defaults to the "
        "subscription’s default payment method, if any, or to the default payment "
        "method in the customer’s invoice settings.",
    )
    # TODO: default_source
    # Note: default_tax_rates is handled in the subclasses since it's a ManyToManyField
    # TODO: discount
    due_date = StripeDateTimeField(
        null=True,
        blank=True,
        help_text=(
            "The date on which payment for this invoice is due. "
            "This value will be null for invoices where billing=charge_automatically."
        ),
    )
    ending_balance = StripeQuantumCurrencyAmountField(
        null=True,
        help_text="Ending customer balance (in cents) after attempting to pay invoice. "
        "If the invoice has not been attempted yet, this will be null.",
    )
    footer = models.TextField(
        max_length=5000, blank=True, help_text="Footer displayed on the invoice."
    )
    # deprecated, will be removed in 2.3
    forgiven = models.NullBooleanField(
        default=False,
        help_text="Whether or not the invoice has been forgiven. "
        "Forgiving an invoice instructs us to update the subscription status as "
        "if the invoice were successfully paid. Once an invoice has been forgiven, "
        "it cannot be unforgiven or reopened.",
    )
    hosted_invoice_url = models.TextField(
        max_length=799,
        default="",
        blank=True,
        help_text="The URL for the hosted invoice page, which allows customers to view "
        "and pay an invoice. If the invoice has not been frozen yet, "
        "this will be null.",
    )
    invoice_pdf = models.TextField(
        max_length=799,
        default="",
        blank=True,
        help_text=(
            "The link to download the PDF for the invoice. "
            "If the invoice has not been frozen yet, this will be null."
        ),
    )
    next_payment_attempt = StripeDateTimeField(
        null=True,
        blank=True,
        help_text="The time at which payment will next be attempted.",
    )
    number = models.CharField(
        max_length=64,
        default="",
        blank=True,
        help_text=(
            "A unique, identifying string that appears on emails sent to the customer "
            "for this invoice. "
            "This starts with the customer’s unique invoice_prefix if it is specified."
        ),
    )
    paid = models.BooleanField(
        default=False, help_text="The time at which payment will next be attempted."
    )
    payment_intent = models.OneToOneField(
        "PaymentIntent",
        on_delete=models.CASCADE,
        null=True,
        help_text=(
            "The PaymentIntent associated with this invoice. "
            "The PaymentIntent is generated when the invoice is finalized, "
            "and can then be used to pay the invoice."
            "Note that voiding an invoice will cancel the PaymentIntent"
        ),
    )
    period_end = StripeDateTimeField(
        help_text="End of the usage period during which invoice items were "
        "added to this invoice."
    )
    period_start = StripeDateTimeField(
        help_text="Start of the usage period during which invoice items were "
        "added to this invoice."
    )
    post_payment_credit_notes_amount = StripeQuantumCurrencyAmountField(
        # This is not nullable, but it's a new field
        null=True,
        blank=True,
        help_text="Total amount (in cents) of all post-payment credit notes issued "
        "for this invoice.",
    )
    pre_payment_credit_notes_amount = StripeQuantumCurrencyAmountField(
        # This is not nullable, but it's a new field
        null=True,
        blank=True,
        help_text="Total amount (in cents) of all pre-payment credit notes issued "
        "for this invoice.",
    )
    receipt_number = models.CharField(
        max_length=64,
        null=True,
        blank=True,
        help_text=(
            "This is the transaction number that appears on email receipts "
            "sent for this invoice."
        ),
    )
    starting_balance = StripeQuantumCurrencyAmountField(
        help_text="Starting customer balance (in cents) before attempting to pay "
        "invoice. If the invoice has not been attempted yet, this will be the "
        "current customer balance."
    )
    statement_descriptor = models.CharField(
        max_length=22,
        default="",
        blank=True,
        help_text="An arbitrary string to be displayed on your customer's "
        "credit card statement. The statement description may not include <>\"' "
        "characters, and will appear on your customer's statement in capital letters. "
        "Non-ASCII characters are automatically stripped. "
        "While most banks display this information consistently, "
        "some may display it incorrectly or not at all.",
    )
    # TODO status
    status_transitions = JSONField(null=True, blank=True)
    subscription = models.ForeignKey(
        "Subscription",
        null=True,
        # we need to use the %(class)s placeholder to avoid related name
        # clashes between Invoice and UpcomingInvoice
        related_name="%(class)ss",
        on_delete=models.SET_NULL,
        help_text="The subscription that this invoice was prepared for, if any.",
    )
    subscription_proration_date = StripeDateTimeField(
        null=True,
        blank=True,
        help_text="Only set for upcoming invoices that preview prorations. "
        "The time used to calculate prorations.",
    )
    subtotal = StripeDecimalCurrencyAmountField(
        help_text="Total (as decimal) of all subscriptions, invoice items, "
        "and prorations on the invoice before any discount or tax is applied."
    )
    tax = StripeDecimalCurrencyAmountField(
        null=True,
        blank=True,
        help_text="The amount (as decimal) of tax included in the total, calculated "
        "from ``tax_percent`` and the subtotal. If no "
        "``tax_percent`` is defined, this value will be null.",
    )
    tax_percent = StripePercentField(
        null=True,
        blank=True,
        help_text="This percentage of the subtotal has been added to the total amount "
        "of the invoice, including invoice line items and discounts. "
        "This field is inherited from the subscription's ``tax_percent`` field, "
        "but can be changed before the invoice is paid. This field defaults to null.",
    )
    threshold_reason = JSONField(
        null=True,
        blank=True,
        help_text="If billing_reason is set to subscription_threshold this returns "
        "more information on which threshold rules triggered the invoice.",
    )
    total = StripeDecimalCurrencyAmountField("Total (as decimal) after discount.")
    webhooks_delivered_at = StripeDateTimeField(
        null=True,
        help_text=(
            "The time at which webhooks for this invoice were successfully delivered "
            "(if the invoice had no webhooks to deliver, this will match `date`). "
            "Invoice payment is delayed until webhooks are delivered, or until all "
            "webhook delivery attempts have been exhausted."
        ),
    )

    class Meta:
        abstract = True
        ordering = ["-created"]

    def __str__(self):
        return "Invoice #{number}".format(
            number=self.number or self.receipt_number or self.id
        )

    @classmethod
    def _manipulate_stripe_object_hook(cls, data):
        data = super()._manipulate_stripe_object_hook(data)
        # Invoice.closed and .forgiven deprecated in API 2018-11-08 -
        # see https://stripe.com/docs/upgrades#2018-11-08

        if "closed" not in data:
            # TODO - drop this in 2.3, use auto_advance instead
            # https://stripe.com/docs/billing/invoices/migrating-new-invoice-states#autoadvance
            if "auto_advance" in data:
                data["closed"] = not data["auto_advance"]
            else:
                data["closed"] = False

        if "forgiven" not in data:
            # TODO - drop this in 2.3, use status == "uncollectible" instead
            if "status" in data:
                data["forgiven"] = data["status"] == "uncollectible"
            else:
                data["forgiven"] = False

        return data

    @classmethod
    def upcoming(
        cls,
        api_key=djstripe_settings.STRIPE_SECRET_KEY,
        customer=None,
        coupon=None,
        subscription=None,
        subscription_plan=None,
        subscription_prorate=None,
        subscription_proration_date=None,
        subscription_quantity=None,
        subscription_trial_end=None,
        **kwargs
    ):
        """
        Gets the upcoming preview invoice (singular) for a customer.

        At any time, you can preview the upcoming
        invoice for a customer. This will show you all the charges that are
        pending, including subscription renewal charges, invoice item charges,
        etc. It will also show you any discount that is applicable to the
        customer. (Source: https://stripe.com/docs/api#upcoming_invoice)

        .. important:: Note that when you are viewing an upcoming invoice,
            you are simply viewing a preview.

        :param customer: The identifier of the customer whose upcoming invoice \
        you'd like to retrieve.
        :type customer: Customer or string (customer ID)
        :param coupon: The code of the coupon to apply.
        :type coupon: str
        :param subscription: The identifier of the subscription to retrieve an \
        invoice for.
        :type subscription: Subscription or string (subscription ID)
        :param subscription_plan: If set, the invoice returned will preview \
        updating the subscription given to this plan, or creating a new \
        subscription to this plan if no subscription is given.
        :type subscription_plan: Plan or string (plan ID)
        :param subscription_prorate: If previewing an update to a subscription, \
        this decides whether the preview will show the result of applying \
        prorations or not.
        :type subscription_prorate: bool
        :param subscription_proration_date: If previewing an update to a \
        subscription, and doing proration, subscription_proration_date forces \
        the proration to be calculated as though the update was done at the \
        specified time.
        :type subscription_proration_date: datetime
        :param subscription_quantity: If provided, the invoice returned will \
        preview updating or creating a subscription with that quantity.
        :type subscription_quantity: int
        :param subscription_trial_end: If provided, the invoice returned will \
        preview updating or creating a subscription with that trial end.
        :type subscription_trial_end: datetime
        :returns: The upcoming preview invoice.
        :rtype: UpcomingInvoice
        """

        # Convert Customer to id
        if customer is not None and isinstance(customer, StripeModel):
            customer = customer.id

        # Convert Subscription to id
        if subscription is not None and isinstance(subscription, StripeModel):
            subscription = subscription.id

        # Convert Plan to id
        if subscription_plan is not None and isinstance(subscription_plan, StripeModel):
            subscription_plan = subscription_plan.id

        try:
            upcoming_stripe_invoice = cls.stripe_class.upcoming(
                api_key=api_key,
                customer=customer,
                coupon=coupon,
                subscription=subscription,
                subscription_plan=subscription_plan,
                subscription_prorate=subscription_prorate,
                subscription_proration_date=subscription_proration_date,
                subscription_quantity=subscription_quantity,
                subscription_trial_end=subscription_trial_end,
                **kwargs
            )
        except InvalidRequestError as exc:
            if str(exc) != "Nothing to invoice for customer":
                raise
            return

        # Workaround for "id" being missing (upcoming invoices don't persist).
        upcoming_stripe_invoice["id"] = "upcoming"

        return UpcomingInvoice._create_from_stripe_object(
            upcoming_stripe_invoice, save=False
        )

    def retry(self):
        """ Retry payment on this invoice if it isn't paid, closed, or forgiven."""

        if not self.paid and not self.forgiven and not self.closed:
            stripe_invoice = self.api_retrieve()
            updated_stripe_invoice = (
                stripe_invoice.pay()
            )  # pay() throws an exception if the charge is not successful.
            type(self).sync_from_stripe_data(updated_stripe_invoice)
            return True
        return False

    STATUS_PAID = "Paid"
    STATUS_FORGIVEN = "Forgiven"
    STATUS_CLOSED = "Closed"
    STATUS_OPEN = "Open"

    @property
    def status(self):
        warnings.warn(
            "Invoice.status will be redefined in djstripe 2.3, use "
            "Invoice.legacy_status to keep the old values",
            DeprecationWarning,
        )

        return self.legacy_status

    @property
    def legacy_status(self):
        """
        Attempts to label this invoice with a status.
        Note that an invoice can be more than one of the choices.
        We just set a priority on which status appears.
        """

        if self.paid:
            return self.STATUS_PAID
        if self.forgiven:
            return self.STATUS_FORGIVEN
        if self.closed:
            return self.STATUS_CLOSED
        return self.STATUS_OPEN

    @property
    def billing(self):
        warnings.warn(
            "Invoice.billing has been renamed to .collection_method. "
            "This alias will be removed in djstripe 2.3",
            DeprecationWarning,
        )
        return self.collection_method

    def get_stripe_dashboard_url(self):
        return self.customer.get_stripe_dashboard_url()

    def _attach_objects_post_save_hook(self, cls, data, pending_relations=None):
        super()._attach_objects_post_save_hook(
            cls, data, pending_relations=pending_relations
        )

        # InvoiceItems need a saved invoice because they're associated via a
        # RelatedManager, so this must be done as part of the post save hook.
        cls._stripe_object_to_invoice_items(
            target_cls=InvoiceItem, data=data, invoice=self
        )

    @property
    def plan(self):
        """ Gets the associated plan for this invoice.

        In order to provide a consistent view of invoices, the plan object
        should be taken from the first invoice item that has one, rather than
        using the plan associated with the subscription.

        Subscriptions (and their associated plan) are updated by the customer
        and represent what is current, but invoice items are immutable within
        the invoice and stay static/unchanged.

        In other words, a plan retrieved from an invoice item will represent
        the plan as it was at the time an invoice was issued.  The plan
        retrieved from the subscription will be the currently active plan.

        :returns: The associated plan for the invoice.
        :rtype: ``djstripe.Plan``
        """

        for invoiceitem in self.invoiceitems.all():
            if invoiceitem.plan:
                return invoiceitem.plan

        if self.subscription:
            return self.subscription.plan


class Invoice(BaseInvoice):
    """
    Invoices are statements of what a customer owes for a particular billing
    period, including subscriptions, invoice items, and any automatic proration
    adjustments if necessary.

    Once an invoice is created, payment is automatically attempted. Note that
    the payment, while automatic, does not happen exactly at the time of invoice
    creation. If you have configured webhooks, the invoice will wait until one
    hour after the last webhook is successfully sent (or the last webhook times
    out after failing).

    Any customer credit on the account is applied before determining how much is
    due for that invoice (the amount that will be actually charged).
    If the amount due for the invoice is less than 50 cents (the minimum for a
    charge), we add the amount to the customer's running account balance to be
    added to the next invoice. If this amount is negative, it will act as a
    credit to offset the next invoice. Note that the customer account balance
    does not include unpaid invoices; it only includes balances that need to be
    taken into account when calculating the amount due for the next invoice.

    Stripe documentation: https://stripe.com/docs/api/python#invoices
    """

    # Note:
    # Most fields are defined on BaseInvoice so they're shared with UpcomingInvoice.
    # ManyToManyFields are an exception, since UpcomingInvoice doesn't exist in the db.
    default_tax_rates = models.ManyToManyField(
        "TaxRate",
        # explicitly specify the joining table name as though the joining model
        # was defined with through="DjstripeInvoiceDefaultTaxRate"
        db_table="djstripe_djstripeinvoicedefaulttaxrate",
        related_name="+",
        blank=True,
        help_text="The tax rates applied to this invoice, if any.",
    )

    def _attach_objects_post_save_hook(self, cls, data, pending_relations=None):
        super()._attach_objects_post_save_hook(
            cls, data, pending_relations=pending_relations
        )

        self.default_tax_rates.set(
            cls._stripe_object_to_default_tax_rates(target_cls=TaxRate, data=data)
        )

        cls._stripe_object_set_total_tax_amounts(
            target_cls=DjstripeInvoiceTotalTaxAmount, data=data, instance=self
        )


class UpcomingInvoice(BaseInvoice):
    """
    The preview of an upcoming invoice - does not exist in the Django database.

    See BaseInvoice.upcoming()

    Logically it should be set abstract, but that doesn't quite work since we
    do actually want to instantiate the model and use relations.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._invoiceitems = []
        self._default_tax_rates = []
        self._total_tax_amounts = []

    def get_stripe_dashboard_url(self):
        return ""

    def _attach_objects_hook(self, cls, data):
        super()._attach_objects_hook(cls, data)
        self._invoiceitems = cls._stripe_object_to_invoice_items(
            target_cls=InvoiceItem, data=data, invoice=self
        )

    def _attach_objects_post_save_hook(self, cls, data, pending_relations=None):
        super()._attach_objects_post_save_hook(
            cls, data, pending_relations=pending_relations
        )

        self._default_tax_rates = cls._stripe_object_to_default_tax_rates(
            target_cls=TaxRate, data=data
        )

        total_tax_amounts = []

        for tax_amount_data in data.get("total_tax_amounts", []):
            tax_rate_data = tax_amount_data["tax_rate"]
            if isinstance(tax_rate_data, str):
                tax_rate_data = {"tax_rate": tax_rate_data}

            tax_rate, _ = TaxRate._get_or_create_from_stripe_object(
                tax_rate_data, field_name="tax_rate", refetch=True
            )

            tax_amount = DjstripeUpcomingInvoiceTotalTaxAmount(
                invoice=self,
                amount=tax_amount_data["amount"],
                inclusive=tax_amount_data["inclusive"],
                tax_rate=tax_rate,
            )

            total_tax_amounts.append(tax_amount)

        self._total_tax_amounts = total_tax_amounts

    @property
    def invoiceitems(self):
        """
        Gets the invoice items associated with this upcoming invoice.

        This differs from normal (non-upcoming) invoices, in that upcoming
        invoices are in-memory and do not persist to the database. Therefore,
        all of the data comes from the Stripe API itself.

        Instead of returning a normal queryset for the invoiceitems, this will
        return a mock of a queryset, but with the data fetched from Stripe - It
        will act like a normal queryset, but mutation will silently fail.
        """

        return QuerySetMock.from_iterable(InvoiceItem, self._invoiceitems)

    @property
    def default_tax_rates(self):
        """
        Gets the default tax rates associated with this upcoming invoice.
        :return:
        """
        return QuerySetMock.from_iterable(TaxRate, self._default_tax_rates)

    @property
    def total_tax_amounts(self):
        """
        Gets the total tax amounts associated with this upcoming invoice.
        :return:
        """
        return QuerySetMock.from_iterable(
            DjstripeUpcomingInvoiceTotalTaxAmount, self._total_tax_amounts
        )

    @property
    def id(self):
        return None

    @id.setter
    def id(self, value):
        return  # noop

    def save(self, *args, **kwargs):
        return  # noop


class InvoiceItem(StripeModel):
    """
    Sometimes you want to add a charge or credit to a customer but only actually
    charge the customer's card at the end of a regular billing cycle.
    This is useful for combining several charges to minimize per-transaction fees
    or having Stripe tabulate your usage-based billing totals.

    Stripe documentation: https://stripe.com/docs/api/python#invoiceitems
    """

    stripe_class = stripe.InvoiceItem

    amount = StripeDecimalCurrencyAmountField(help_text="Amount invoiced (as decimal).")
    currency = StripeCurrencyCodeField()
    customer = models.ForeignKey(
        "Customer",
        on_delete=models.CASCADE,
        related_name="invoiceitems",
        help_text="The customer associated with this invoiceitem.",
    )
    date = StripeDateTimeField(help_text="The date on the invoiceitem.")
    discountable = models.BooleanField(
        default=False,
        help_text="If True, discounts will apply to this invoice item. "
        "Always False for prorations.",
    )
    invoice = models.ForeignKey(
        "Invoice",
        on_delete=models.CASCADE,
        null=True,
        related_name="invoiceitems",
        help_text="The invoice to which this invoiceitem is attached.",
    )
    period = JSONField()
    period_end = StripeDateTimeField(
        help_text="Might be the date when this invoiceitem's invoice was sent."
    )
    period_start = StripeDateTimeField(
        help_text="Might be the date when this invoiceitem was added to the invoice"
    )
    plan = models.ForeignKey(
        "Plan",
        null=True,
        related_name="invoiceitems",
        on_delete=models.SET_NULL,
        help_text="If the invoice item is a proration, the plan of the subscription "
        "for which the proration was computed.",
    )
    proration = models.BooleanField(
        default=False,
        help_text="Whether or not the invoice item was created automatically as a "
        "proration adjustment when the customer switched plans.",
    )
    quantity = models.IntegerField(
        null=True,
        blank=True,
        help_text="If the invoice item is a proration, the quantity of the "
        "subscription for which the proration was computed.",
    )
    subscription = models.ForeignKey(
        "Subscription",
        null=True,
        related_name="invoiceitems",
        on_delete=models.SET_NULL,
        help_text="The subscription that this invoice item has been created for, "
        "if any.",
    )
    # XXX: subscription_item
    tax_rates = models.ManyToManyField(
        "TaxRate",
        # explicitly specify the joining table name as though the joining model
        # was defined with through="DjstripeInvoiceItemTaxRate"
        db_table="djstripe_djstripeinvoiceitemtaxrate",
        related_name="+",
        blank=True,
        help_text="The tax rates which apply to this invoice item. When set, "
        "the default_tax_rates on the invoice do not apply to this "
        "invoice item.",
    )

    @classmethod
    def _manipulate_stripe_object_hook(cls, data):
        data["period_start"] = data["period"]["start"]
        data["period_end"] = data["period"]["end"]

        return data

    def _attach_objects_post_save_hook(self, cls, data, pending_relations=None):
        super()._attach_objects_post_save_hook(
            cls, data, pending_relations=pending_relations
        )

        if self.pk:
            # only call .set() on saved instance (ie don't on items of UpcomingInvoice)
            self.tax_rates.set(
                cls._stripe_object_to_tax_rates(target_cls=TaxRate, data=data)
            )

    @classmethod
    def sync_from_stripe_data(cls, data):
        invoice_data = data.get("invoice")

        if invoice_data:
            # sync the Invoice first if it doesn't yet exist in our DB
            # to avoid recursive Charge/Invoice loop
            invoice_id = cls._id_from_data(invoice_data)
            if not Invoice.objects.filter(id=invoice_id).exists():
                if invoice_id == invoice_data:
                    # we only have the id, fetch the full data
                    invoice_data = Invoice(id=invoice_id).api_retrieve()
                Invoice.sync_from_stripe_data(data=invoice_data)

        return super().sync_from_stripe_data(data)

    def __str__(self):
        if self.plan and self.plan.product:
            return self.plan.product.name or str(self.plan)
        return super().__str__()

    @classmethod
    def is_valid_object(cls, data):
        return data["object"] in ("invoiceitem", "line_item")

    def get_stripe_dashboard_url(self):
        return self.invoice.get_stripe_dashboard_url()

    def str_parts(self):
        return [
            "amount={amount}".format(amount=self.amount),
            "date={date}".format(date=self.date),
        ] + super().str_parts()


class Plan(StripeModel):
    """
    A subscription plan contains the pricing information for different
    products and feature levels on your site.

    Stripe documentation: https://stripe.com/docs/api/python#plans)
    """

    stripe_class = stripe.Plan
    stripe_dashboard_item_name = "plans"

    active = models.BooleanField(
        help_text="Whether the plan is currently available for new subscriptions."
    )
    aggregate_usage = StripeEnumField(
        enum=enums.PlanAggregateUsage,
        default="",
        blank=True,
        help_text=(
            "Specifies a usage aggregation strategy for plans of usage_type=metered. "
            "Allowed values are `sum` for summing up all usage during a period, "
            "`last_during_period` for picking the last usage record reported within a "
            "period, `last_ever` for picking the last usage record ever (across period "
            "bounds) or max which picks the usage record with the maximum reported "
            "usage during a period. Defaults to `sum`."
        ),
    )
    amount = StripeDecimalCurrencyAmountField(
        null=True,
        blank=True,
        help_text="Amount (as decimal) to be charged on the interval specified.",
    )
    billing_scheme = StripeEnumField(
        enum=enums.PlanBillingScheme,
        default="",
        blank=True,
        help_text=(
            "Describes how to compute the price per period. "
            "Either `per_unit` or `tiered`. "
            "`per_unit` indicates that the fixed amount (specified in amount) "
            "will be charged per unit in quantity "
            "(for plans with `usage_type=licensed`), or per unit of total "
            "usage (for plans with `usage_type=metered`). "
            "`tiered` indicates that the unit pricing will be computed using "
            "a tiering strategy as defined using the tiers and tiers_mode attributes."
        ),
    )
    currency = StripeCurrencyCodeField()
    interval = StripeEnumField(
        enum=enums.PlanInterval,
        help_text="The frequency with which a subscription should be billed.",
    )
    interval_count = models.IntegerField(
        null=True,
        help_text=(
            "The number of intervals (specified in the interval property) "
            "between each subscription billing."
        ),
    )
    nickname = models.TextField(
        max_length=5000,
        default="",
        blank=True,
        help_text="A brief description of the plan, hidden from customers.",
    )
    product = models.ForeignKey(
        "Product",
        on_delete=models.SET_NULL,
        null=True,
        help_text="The product whose pricing this plan determines.",
    )
    tiers = JSONField(
        null=True,
        blank=True,
        help_text=(
            "Each element represents a pricing tier. "
            "This parameter requires `billing_scheme` to be set to `tiered`."
        ),
    )
    tiers_mode = StripeEnumField(
        enum=enums.PlanTiersMode,
        null=True,
        blank=True,
        help_text=(
            "Defines if the tiering price should be `graduated` or `volume` based. "
            "In `volume`-based tiering, the maximum quantity within a period "
            "determines the per unit price, in `graduated` tiering pricing can "
            "successively change as the quantity grows."
        ),
    )
    transform_usage = JSONField(
        null=True,
        blank=True,
        help_text=(
            "Apply a transformation to the reported usage or set quantity "
            "before computing the billed price. Cannot be combined with `tiers`."
        ),
    )
    trial_period_days = models.IntegerField(
        null=True,
        help_text=(
            "Number of trial period days granted when subscribing a customer "
            "to this plan. Null if the plan has no trial period."
        ),
    )
    usage_type = StripeEnumField(
        enum=enums.PlanUsageType,
        default=enums.PlanUsageType.licensed,
        help_text=(
            "Configures how the quantity per period should be determined, "
            "can be either `metered` or `licensed`. `licensed` will automatically "
            "bill the `quantity` set for a plan when adding it to a subscription, "
            "`metered` will aggregate the total usage based on usage records. "
            "Defaults to `licensed`."
        ),
    )

    # Legacy fields (pre 2017-08-15)
    name = models.TextField(
        null=True,
        blank=True,
        help_text="Name of the plan, to be displayed on invoices and in "
        "the web interface.",
    )
    statement_descriptor = models.CharField(
        max_length=22,
        null=True,
        blank=True,
        help_text="An arbitrary string to be displayed on your customer's credit card "
        "statement. The statement description may not include <>\"' characters, "
        "and will appear on your customer's statement in capital letters. "
        "Non-ASCII characters are automatically stripped. "
        "While most banks display this information consistently, some may display it "
        "incorrectly or not at all.",
    )

    class Meta(object):
        ordering = ["amount"]

    @classmethod
    def get_or_create(cls, **kwargs):
        """ Get or create a Plan."""

        try:
            return Plan.objects.get(id=kwargs["id"]), False
        except Plan.DoesNotExist:
            return cls.create(**kwargs), True

    @classmethod
    def create(cls, **kwargs):
        # A few minor things are changed in the api-version of the create call
        api_kwargs = dict(kwargs)
        api_kwargs["amount"] = int(api_kwargs["amount"] * 100)

        if isinstance(api_kwargs.get("product"), StripeModel):
            api_kwargs["product"] = api_kwargs["product"].id

        stripe_plan = cls._api_create(**api_kwargs)
        plan = cls.sync_from_stripe_data(stripe_plan)

        return plan

    def __str__(self):
        return self.name or self.nickname or self.id

    @property
    def amount_in_cents(self):
        return int(self.amount * 100)

    @property
    def human_readable_price(self):
        amount = get_friendly_currency_amount(self.amount, self.currency)
        interval_count = self.interval_count

        if interval_count == 1:
            interval = {
                "day": _("day"),
                "week": _("week"),
                "month": _("month"),
                "year": _("year"),
            }[self.interval]
            template = _("{amount}/{interval}")
        else:
            interval = {
                "day": _("days"),
                "week": _("weeks"),
                "month": _("months"),
                "year": _("years"),
            }[self.interval]
            template = _("{amount} every {interval_count} {interval}")

        return format_lazy(
            template, amount=amount, interval=interval, interval_count=interval_count
        )

    # TODO: Move this type of update to the model's save() method
    #  so it happens automatically
    #  Also, block other fields from being saved.
    def update_name(self):
        """
        Update the name of the Plan in Stripe and in the db.

        Assumes the object being called has the name attribute already
        reset, but has not been saved.

        Stripe does not allow for update of any other Plan attributes besides name.
        """

        p = self.api_retrieve()
        p.name = self.name
        p.save()

        self.save()


class Subscription(StripeModel):
    """
    Subscriptions allow you to charge a customer's card on a recurring basis.
    A subscription ties a customer to a particular plan you've created.

    A subscription still in its trial period is ``trialing`` and moves to ``active``
    when the trial period is over.

    When payment to renew the subscription fails, the subscription becomes ``past_due``.
    After Stripe has exhausted all payment retry attempts, the subscription ends up
    with a status of either ``canceled`` or ``unpaid`` depending on your retry settings.

    Note that when a subscription has a status of ``unpaid``, no subsequent invoices
    will be attempted (invoices will be created, but then immediately
    automatically closed.

    Additionally, updating customer card details will not lead to Stripe retrying the
    latest invoice.).
    After receiving updated card details from a customer, you may choose to reopen and
    pay their closed invoices.

    Stripe documentation: https://stripe.com/docs/api/python#subscriptions
    """

    stripe_class = stripe.Subscription
    stripe_dashboard_item_name = "subscriptions"

    application_fee_percent = StripePercentField(
        null=True,
        blank=True,
        help_text="A positive decimal that represents the fee percentage of the "
        "subscription invoice amount that will be transferred to the application "
        "owner's Stripe account each billing period.",
    )
    billing = StripeEnumField(
        enum=enums.InvoiceBilling,
        help_text="Either `charge_automatically`, or `send_invoice`. When charging "
        "automatically, Stripe will attempt to pay this subscription at the end of the "
        "cycle using the default source attached to the customer. "
        "When sending an invoice, Stripe will email your customer an invoice with "
        "payment instructions.",
    )
    billing_cycle_anchor = StripeDateTimeField(
        null=True,
        blank=True,
        help_text=(
            "Determines the date of the first full invoice, and, for plans "
            "with `month` or `year` intervals, the day of the month for subsequent "
            "invoices."
        ),
    )
    cancel_at_period_end = models.BooleanField(
        default=False,
        help_text="If the subscription has been canceled with the ``at_period_end`` "
        "flag set to true, ``cancel_at_period_end`` on the subscription will be true. "
        "You can use this attribute to determine whether a subscription that has a "
        "status of active is scheduled to be canceled at the end of the "
        "current period.",
    )
    canceled_at = StripeDateTimeField(
        null=True,
        blank=True,
        help_text="If the subscription has been canceled, the date of that "
        "cancellation. If the subscription was canceled with ``cancel_at_period_end``, "
        "canceled_at will still reflect the date of the initial cancellation request, "
        "not the end of the subscription period when the subscription is automatically "
        "moved to a canceled state.",
    )
    current_period_end = StripeDateTimeField(
        help_text="End of the current period for which the subscription has been "
        "invoiced. At the end of this period, a new invoice will be created."
    )
    current_period_start = StripeDateTimeField(
        help_text="Start of the current period for which the subscription has "
        "been invoiced."
    )
    customer = models.ForeignKey(
        "Customer",
        on_delete=models.CASCADE,
        related_name="subscriptions",
        help_text="The customer associated with this subscription.",
    )
    days_until_due = models.IntegerField(
        null=True,
        blank=True,
        help_text="Number of days a customer has to pay invoices generated by this "
        "subscription. This value will be `null` for subscriptions where "
        "`billing=charge_automatically`.",
    )
    default_tax_rates = models.ManyToManyField(
        "TaxRate",
        # explicitly specify the joining table name as though the joining model
        # was defined with through="DjstripeSubscriptionDefaultTaxRate"
        db_table="djstripe_djstripesubscriptiondefaulttaxrate",
        related_name="+",
        blank=True,
        help_text="The tax rates that will apply to any subscription item "
        "that does not have tax_rates set. Invoices created will have their "
        "default_tax_rates populated from the subscription.",
    )
    # TODO: discount
    ended_at = StripeDateTimeField(
        null=True,
        blank=True,
        help_text="If the subscription has ended (either because it was canceled or "
        "because the customer was switched to a subscription to a new plan), "
        "the date the subscription ended.",
    )
    pending_setup_intent = models.ForeignKey(
        "SetupIntent",
        null=True,
        blank=True,
        on_delete=models.CASCADE,
        related_name="setup_intents",
        help_text="We can use this SetupIntent to collect user authentication "
        "when creating a subscription without immediate payment or updating a "
        "subscription’s payment method, allowing you to "
        "optimize for off-session payments.",
    )
    plan = models.ForeignKey(
        "Plan",
        null=True,
        blank=True,
        on_delete=models.CASCADE,
        related_name="subscriptions",
        help_text="The plan associated with this subscription. This value will be "
        "`null` for multi-plan subscriptions",
    )
    quantity = models.IntegerField(
        null=True,
        blank=True,
        help_text="The quantity applied to this subscription. This value will be "
        "`null` for multi-plan subscriptions",
    )
    start = StripeDateTimeField(
        help_text="Date of the last substantial change to "
        "this subscription. For example, a change to the items array, or a change "
        "of status, will reset this timestamp."
    )
    status = StripeEnumField(
        enum=enums.SubscriptionStatus, help_text="The status of this subscription."
    )
    tax_percent = StripePercentField(
        null=True,
        blank=True,
        help_text="A positive decimal (with at most two decimal places) "
        "between 1 and 100. This represents the percentage of the subscription "
        "invoice subtotal that will be calculated and added as tax to the final "
        "amount each billing period.",
    )
    trial_end = StripeDateTimeField(
        null=True,
        blank=True,
        help_text="If the subscription has a trial, the end of that trial.",
    )
    trial_start = StripeDateTimeField(
        null=True,
        blank=True,
        help_text="If the subscription has a trial, the beginning of that trial.",
    )

    objects = SubscriptionManager()

    def __str__(self):
        return "{customer} on {plan}".format(
            customer=str(self.customer), plan=str(self.plan)
        )

    def update(
        self,
        plan=None,
        application_fee_percent=None,
        billing_cycle_anchor=None,
        coupon=None,
        prorate=djstripe_settings.PRORATION_POLICY,
        proration_date=None,
        metadata=None,
        quantity=None,
        tax_percent=None,
        trial_end=None,
    ):
        """
        See `Customer.subscribe() <#djstripe.models.Customer.subscribe>`__

        :param plan: The plan to which to subscribe the customer.
        :type plan: Plan or string (plan ID)
        :param application_fee_percent:
        :type application_fee_percent:
        :param billing_cycle_anchor:
        :type billing_cycle_anchor:
        :param coupon:
        :type coupon:
        :param prorate: Whether or not to prorate when switching plans. Default is True.
        :type prorate: boolean
        :param proration_date:
            If set, the proration will be calculated as though the subscription was
            updated at the given time. This can be used to apply exactly the same
            proration that was previewed with upcoming invoice endpoint.
            It can also be used to implement custom proration logic, such as prorating
            by day instead of by second, by providing the time that you
            wish to use for proration calculations.
        :type proration_date: datetime
        :param metadata:
        :type metadata:
        :param quantity:
        :type quantity:
        :param tax_percent:
        :type tax_percent:
        :param trial_end:
        :type trial_end:

        .. note:: The default value for ``prorate`` is the DJSTRIPE_PRORATION_POLICY \
            setting.

        .. important:: Updating a subscription by changing the plan or quantity \
            creates a new ``Subscription`` in \
            Stripe (and dj-stripe).
        """

        # Convert Plan to id
        if plan is not None and isinstance(plan, StripeModel):
            plan = plan.id

        kwargs = deepcopy(locals())
        del kwargs["self"]

        stripe_subscription = self.api_retrieve()

        for kwarg, value in kwargs.items():
            if value is not None:
                setattr(stripe_subscription, kwarg, value)

        return Subscription.sync_from_stripe_data(stripe_subscription.save())

    def extend(self, delta):
        """
        Extends this subscription by the provided delta.

        :param delta: The timedelta by which to extend this subscription.
        :type delta: timedelta
        """

        if delta.total_seconds() < 0:
            raise ValueError("delta must be a positive timedelta.")

        if self.trial_end is not None and self.trial_end > timezone.now():
            period_end = self.trial_end
        else:
            period_end = self.current_period_end

        period_end += delta

        return self.update(prorate=False, trial_end=period_end)

    def cancel(self, at_period_end=djstripe_settings.CANCELLATION_AT_PERIOD_END):
        """
        Cancels this subscription. If you set the at_period_end parameter to true,
        the subscription will remain active until the end of the period, at which point
        it will be canceled and not renewed. By default, the subscription is terminated
        immediately. In either case, the customer will not be charged again for
        the subscription. Note, however, that any pending invoice items that you've
        created will still be charged for at the end of the period unless manually
        deleted. If you've set the subscription to cancel at period end,
        any pending prorations will also be left in place and collected at the end of
        the period, but if the subscription is set to cancel immediately,
        pending prorations will be removed.

        By default, all unpaid invoices for the customer will be closed upon
        subscription cancellation. We do this in order to prevent unexpected payment
        retries once the customer has canceled a subscription. However, you can
        reopen the invoices manually after subscription cancellation to have us proceed
        with automatic retries, or you could even re-attempt payment yourself on all
        unpaid invoices before allowing the customer to cancel the
        subscription at all.

        :param at_period_end: A flag that if set to true will delay the cancellation \
            of the subscription until the end of the current period. Default is False.
        :type at_period_end: boolean

        .. important:: If a subscription is canceled during a trial period, \
        the ``at_period_end`` flag will be overridden to False so that the trial ends \
        immediately and the customer's card isn't charged.
        """

        # If plan has trial days and customer cancels before
        # trial period ends, then end subscription now,
        # i.e. at_period_end=False
        if self.trial_end and self.trial_end > timezone.now():
            at_period_end = False

        if at_period_end:
            stripe_subscription = self.api_retrieve()
            stripe_subscription.cancel_at_period_end = True
            stripe_subscription.save()
        else:
            try:
                stripe_subscription = self._api_delete()
            except InvalidRequestError as exc:
                if "No such subscription:" in str(exc):
                    # cancel() works by deleting the subscription. The object still
                    # exists in Stripe however, and can still be retrieved.
                    # If the subscription was already canceled (status=canceled),
                    # that api_retrieve() call will fail with "No such subscription".
                    # However, this may also happen if the subscription legitimately
                    # does not exist, in which case the following line will re-raise.
                    stripe_subscription = self.api_retrieve()
                else:
                    raise

        return Subscription.sync_from_stripe_data(stripe_subscription)

    def reactivate(self):
        """
        Reactivates this subscription.

        If a customer's subscription is canceled with ``at_period_end`` set to True and
        it has not yet reached the end of the billing period, it can be reactivated.
        Subscriptions canceled immediately cannot be reactivated.
        (Source: https://stripe.com/docs/subscriptions/canceling-pausing)

        .. warning:: Reactivating a fully canceled Subscription will fail silently. \
        Be sure to check the returned Subscription's status.
        """
        stripe_subscription = self.api_retrieve()
        stripe_subscription.plan = self.plan.id
        stripe_subscription.cancel_at_period_end = False

        return Subscription.sync_from_stripe_data(stripe_subscription.save())

    def is_period_current(self):
        """
        Returns True if this subscription's period is current, false otherwise.
        """

        return self.current_period_end > timezone.now() or (
            self.trial_end and self.trial_end > timezone.now()
        )

    def is_status_current(self):
        """
        Returns True if this subscription's status is current (active or trialing),
        false otherwise.
        """

        return self.status in ["trialing", "active"]

    def is_status_temporarily_current(self):
        """
        A status is temporarily current when the subscription is canceled with the
        ``at_period_end`` flag.
        The subscription is still active, but is technically canceled and we're just
        waiting for it to run out.

        You could use this method to give customers limited service after they've
        canceled. For example, a video on demand service could only allow customers
        to download their libraries and do nothing else when their
        subscription is temporarily current.
        """

        return (
            self.canceled_at
            and self.cancel_at_period_end
            and timezone.now() < self.current_period_end
        )

    def is_valid(self):
        """
        Returns True if this subscription's status and period are current,
        false otherwise.
        """

        if not self.is_status_current():
            return False

        if not self.is_period_current():
            return False

        return True

    def _attach_objects_post_save_hook(self, cls, data, pending_relations=None):
        super()._attach_objects_post_save_hook(
            cls, data, pending_relations=pending_relations
        )

        cls._stripe_object_to_subscription_items(
            target_cls=SubscriptionItem, data=data, subscription=self
        )

        self.default_tax_rates.set(
            cls._stripe_object_to_default_tax_rates(target_cls=TaxRate, data=data)
        )


class SubscriptionItem(StripeModel):
    """
    Subscription items allow you to create customer subscriptions
    with more than one plan, making it easy to represent complex billing relationships.

    Stripe documentation: https://stripe.com/docs/api#subscription_items
    """

    stripe_class = stripe.SubscriptionItem

    plan = models.ForeignKey(
        "Plan",
        on_delete=models.CASCADE,
        related_name="subscription_items",
        help_text="The plan the customer is subscribed to.",
    )
    quantity = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text=(
            "The quantity of the plan to which the customer should be subscribed."
        ),
    )
    subscription = models.ForeignKey(
        "Subscription",
        on_delete=models.CASCADE,
        related_name="items",
        help_text="The subscription this subscription item belongs to.",
    )
    tax_rates = models.ManyToManyField(
        "TaxRate",
        # explicitly specify the joining table name as though the joining model
        # was defined with through="DjstripeSubscriptionItemTaxRate"
        db_table="djstripe_djstripesubscriptionitemtaxrate",
        related_name="+",
        blank=True,
        help_text="The tax rates which apply to this subscription_item. When set, "
        "the default_tax_rates on the subscription do not apply to this "
        "subscription_item.",
    )

    def _attach_objects_post_save_hook(self, cls, data, pending_relations=None):
        super()._attach_objects_post_save_hook(
            cls, data, pending_relations=pending_relations
        )

        self.tax_rates.set(
            cls._stripe_object_to_tax_rates(target_cls=TaxRate, data=data)
        )


class TaxRate(StripeModel):
    """
    Tax rates can be applied to invoices and subscriptions to collect tax.

    Stripe documentation: https://stripe.com/docs/api/tax_rates
    """

    stripe_class = stripe.TaxRate

    active = models.BooleanField(
        default=True,
        help_text="Defaults to true. When set to false, this tax rate cannot be "
        "applied to objects in the API, but will still be applied to subscriptions "
        "and invoices that already have it set.",
    )
    display_name = models.CharField(
        max_length=50,
        default="",
        blank=True,
        help_text="The display name of the tax rates as it will appear to your "
        "customer on their receipt email, PDF, and the hosted invoice page.",
    )
    inclusive = models.BooleanField(
        help_text="This specifies if the tax rate is inclusive or exclusive."
    )
    jurisdiction = models.CharField(
        max_length=50,
        default="",
        blank=True,
        help_text="The jurisdiction for the tax rate.",
    )
    percentage = StripePercentField(
        help_text="This represents the tax rate percent out of 100."
    )


class UsageRecord(StripeModel):
    """
    Usage records allow you to continually report usage and metrics to
    Stripe for metered billing of plans.

    Stripe documentation: https://stripe.com/docs/api#usage_records
    """

    quantity = models.PositiveIntegerField(
        help_text=(
            "The quantity of the plan to which the customer should be subscribed."
        )
    )
    subscription_item = models.ForeignKey(
        "SubscriptionItem",
        on_delete=models.CASCADE,
        related_name="usage_records",
        help_text="The subscription item this usage record contains data for.",
    )
