text: from copy import copy, deepcopy
  from dataclasses import dataclass
  from decimal import Decimal
  from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Optional, Tuple, Union
  
  from django.core.handlers.wsgi import WSGIRequest
  from django.http import HttpResponse
  from django_countries.fields import Country
  from measurement.measures import Weight
  from prices import Money, TaxedMoney
  
  from ..payment.interface import (
      CustomerSource,
      GatewayResponse,
      InitializedPaymentResponse,
      PaymentData,
      PaymentGateway,
  )
  from .models import PluginConfiguration
  
  if TYPE_CHECKING:
      # flake8: noqa
      from ..account.models import Address, User
      from ..channel.models import Channel
      from ..checkout.fetch import CheckoutInfo, CheckoutLineInfo
      from ..checkout.models import Checkout
      from ..core.notify_events import NotifyEventType
      from ..core.taxes import TaxType
      from ..discount import DiscountInfo
      from ..invoice.models import Invoice
      from ..order.models import Fulfillment, Order, OrderLine
      from ..page.models import Page
      from ..product.models import Product, ProductType, ProductVariant
  
  PluginConfigurationType = List[dict]
  NoneType = type(None)
  
  
  class ConfigurationTypeField:
      STRING = "String"
      MULTILINE = "Multiline"
      BOOLEAN = "Boolean"
      SECRET = "Secret"
      SECRET_MULTILINE = "SecretMultiline"
      PASSWORD = "Password"
      OUTPUT = "OUTPUT"
      CHOICES = [
          (STRING, "Field is a String"),
          (MULTILINE, "Field is a Multiline"),
          (BOOLEAN, "Field is a Boolean"),
          (SECRET, "Field is a Secret"),
          (PASSWORD, "Field is a Password"),
          (SECRET_MULTILINE, "Field is a Secret multiline"),
          (OUTPUT, "Field is a read only"),
      ]
  
  
  @dataclass
  class ExternalAccessTokens:
      token: Optional[str] = None
      refresh_token: Optional[str] = None
      csrf_token: Optional[str] = None
      user: Optional["User"] = None
  
  
  @dataclass
  class ExcludedShippingMethod:
      id: str
      reason: Optional[str]
  
  
  @dataclass
  class ShippingMethod:
      id: str
      price: Money
      name: str
      maximum_order_weight: Optional[Weight]
      minimum_order_weight: Optional[Weight]
      maximum_delivery_days: Optional[int]
      minimum_delivery_days: Optional[int]
  
  
  class BasePlugin:
      """Abstract class for storing all methods available for any plugin.
  
      All methods take previous_value parameter.
      previous_value contains a value calculated by the previous plugin in the queue.
      If the plugin is first, it will use default value calculated by the manager.
      """
  
      PLUGIN_NAME = ""
      PLUGIN_ID = ""
      PLUGIN_DESCRIPTION = ""
      CONFIG_STRUCTURE = None
      CONFIGURATION_PER_CHANNEL = True
      DEFAULT_CONFIGURATION = []
      DEFAULT_ACTIVE = False
  
      @classmethod
      def check_plugin_id(cls, plugin_id: str) -> bool:
          """Check if given plugin_id matches with the PLUGIN_ID of this plugin."""
          return cls.PLUGIN_ID == plugin_id
  
      def __init__(
          self,
          *,
          configuration: PluginConfigurationType,
          active: bool,
          channel: Optional["Channel"] = None
      ):
          self.configuration = self.get_plugin_configuration(configuration)
          self.active = active
          self.channel = channel
  
      def __str__(self):
          return self.PLUGIN_NAME
  
      #  Apply taxes to the product price based on the customer country.
      #
      #  Overwrite this method if you want to show products with taxes.
      apply_taxes_to_product: Callable[
          ["Product", Money, Country, TaxedMoney], TaxedMoney
      ]
  
      #  Apply taxes to the shipping costs based on the shipping address.
      #
      #  Overwrite this method if you want to show available shipping methods with
      #  taxes.
      apply_taxes_to_shipping: Callable[[Money, "Address", TaxedMoney], TaxedMoney]
  
      #  Assign tax code dedicated to plugin.
      assign_tax_code_to_object_meta: Callable[
          [Union["Product", "ProductType"], Union[str, NoneType], Any], Any
      ]
  
      #  Authenticate user which should be assigned to the request.
      #
      #  Overwrite this method if the plugin handles authentication flow.
      authenticate_user: Callable[[WSGIRequest], Union["User", NoneType]]
  
      authorize_payment: Callable[["PaymentData", Any], GatewayResponse]
  
      #  Calculate checkout line total.
      #
      #  Overwrite this method if you need to apply specific logic for the calculation
      #  of a checkout line total. Return TaxedMoney.
      calculate_checkout_line_total: Callable[
          [
              "CheckoutInfo",
              List["CheckoutLineInfo"],
              "CheckoutLineInfo",
              Union["Address", NoneType],
              Iterable["DiscountInfo"],
              TaxedMoney,
          ],
          TaxedMoney,
      ]
  
      #  Calculate checkout line unit price.
      calculate_checkout_line_unit_price: Callable[
          [
              "CheckoutInfo",
              List["CheckoutLineInfo"],
              "CheckoutLineInfo",
              Union["Address", NoneType],
              Iterable["DiscountInfo"],
              Any,
          ],
          Any,
      ]
  
      #  Calculate the shipping costs for checkout.
      #
      #  Overwrite this method if you need to apply specific logic for the calculation
      #  of shipping costs. Return TaxedMoney.
      calculate_checkout_shipping: Callable[
          [
              "CheckoutInfo",
              List["CheckoutLineInfo"],
              Union["Address", NoneType],
              List["DiscountInfo"],
              TaxedMoney,
          ],
          TaxedMoney,
      ]
  
      #  Calculate the total for checkout.
      #
      #  Overwrite this method if you need to apply specific logic for the calculation
      #  of a checkout total. Return TaxedMoney.
      calculate_checkout_total: Callable[
          [
              "CheckoutInfo",
              List["CheckoutLineInfo"],
              Union["Address", NoneType],
              List["DiscountInfo"],
              TaxedMoney,
          ],
          TaxedMoney,
      ]
  
      #  Calculate order line total.
      #
      #  Overwrite this method if you need to apply specific logic for the calculation
      #  of a order line total. Return TaxedMoney.
      calculate_order_line_total: Callable[
          ["Order", "OrderLine", "ProductVariant", "Product", TaxedMoney], TaxedMoney
      ]
  
      #  Calculate order line unit price.
      #
      #  Update order line unit price in the order in case of changes in draft order.
      #  Return TaxedMoney.
      #  Overwrite this method if you need to apply specific logic for the calculation
      #  of an order line unit price.
      calculate_order_line_unit: Callable[
          ["Order", "OrderLine", "ProductVariant", "Product", TaxedMoney], TaxedMoney
      ]
  
      #  Calculate the shipping costs for the order.
      #
      #  Update shipping costs in the order in case of changes in shipping address or
      #  changes in draft order. Return TaxedMoney.
      calculate_order_shipping: Callable[["Order", TaxedMoney], TaxedMoney]
  
      capture_payment: Callable[["PaymentData", Any], GatewayResponse]
  
      change_user_address: Callable[
          ["Address", Union[str, NoneType], Union["User", NoneType], "Address"], "Address"
      ]
  
      #  Retrieves the balance remaining on a shopper's gift card
      check_payment_balance: Callable[[dict, str], dict]
  
      #  Trigger when checkout is created.
      #
      #  Overwrite this method if you need to trigger specific logic when a checkout is
      #  created.
      checkout_created: Callable[["Checkout", Any], Any]
  
      #  Trigger when checkout is updated.
      #
      #  Overwrite this method if you need to trigger specific logic when a checkout is
      #  updated.
      checkout_updated: Callable[["Checkout", Any], Any]
  
      confirm_payment: Callable[["PaymentData", Any], GatewayResponse]
  
      #  Trigger when user is created.
      #
      #  Overwrite this method if you need to trigger specific logic after a user is
      #  created.
      customer_created: Callable[["User", Any], Any]
  
      #  Trigger when user is updated.
      #
      #  Overwrite this method if you need to trigger specific logic after a user is
      #  updated.
      customer_updated: Callable[["User", Any], Any]
  
      #  Handle authentication request.
      #
      #  Overwrite this method if the plugin handles authentication flow.
      external_authentication_url: Callable[[dict, WSGIRequest], dict]
  
      #  Handle logout request.
      #
      #  Overwrite this method if the plugin handles logout flow.
      external_logout: Callable[[dict], Any]
  
      #  Handle authentication request responsible for obtaining access tokens.
      #
      #  Overwrite this method if the plugin handles authentication flow.
      external_obtain_access_tokens: Callable[[dict, WSGIRequest], ExternalAccessTokens]
  
      #  Handle authentication refresh request.
      #
      #  Overwrite this method if the plugin handles authentication flow and supports
      #  refreshing the access.
      external_refresh: Callable[[dict, WSGIRequest], ExternalAccessTokens]
  
      #  Verify the provided authentication data.
      #
      #  Overwrite this method if the plugin should validate the authentication data.
      external_verify: Callable[[dict, WSGIRequest], Tuple[Union["User", NoneType], dict]]
  
      #  Triggered when ShopFetchTaxRates mutation is called.
      fetch_taxes_data: Callable[[Any], Any]
  
      #  Trigger when fulfillemnt is created.
      #
      #  Overwrite this method if you need to trigger specific logic when a fulfillment is
      #  created.
      fulfillment_created: Callable[["Fulfillment", Any], Any]
  
      get_checkout_line_tax_rate: Callable[
          [
              "CheckoutInfo",
              List["CheckoutLineInfo"],
              "CheckoutLineInfo",
              Union["Address", NoneType],
              Iterable["DiscountInfo"],
              Decimal,
          ],
          Decimal,
      ]
  
      get_checkout_shipping_tax_rate: Callable[
          [
              "CheckoutInfo",
              Iterable["CheckoutLineInfo"],
              Union["Address", NoneType],
              Iterable["DiscountInfo"],
              Any,
          ],
          Any,
      ]
  
      get_client_token: Callable[[Any, Any], Any]
  
      get_order_line_tax_rate: Callable[
          ["Order", "Product", "ProductVariant", Union["Address", NoneType], Decimal],
          Decimal,
      ]
  
      get_order_shipping_tax_rate: Callable[["Order", Any], Any]
      get_payment_config: Callable[[Any], Any]
  
      get_supported_currencies: Callable[[Any], Any]
  
      #  Return tax code from object meta.
      get_tax_code_from_object_meta: Callable[
          [Union["Product", "ProductType"], "TaxType"], "TaxType"
      ]
  
      #  Return tax rate percentage value for a given tax rate type in a country.
      #
      #  It is used only by the old storefront.
      get_tax_rate_percentage_value: Callable[
          [Union["Product", "ProductType"], Country, Any], Decimal
      ]
  
      #  Return list of all tax categories.
      #
      #  The returned list will be used to provide staff users with the possibility to
      #  assign tax categories to a product. It can be used by tax plugins to properly
      #  calculate taxes for products.
      #  Overwrite this method in case your plugin provides a list of tax categories.
      get_tax_rate_type_choices: Callable[[List["TaxType"]], List["TaxType"]]
  
      initialize_payment: Callable[[dict], InitializedPaymentResponse]
  
      #  Trigger before invoice is deleted.
      #
      #  Perform any extra logic before the invoice gets deleted.
      #  Note there is no need to run invoice.delete() as it will happen in mutation.
      invoice_delete: Callable[["Invoice", Any], Any]
  
      #  Trigger when invoice creation starts.
      #  May return Invoice object.
      #  Overwrite to create invoice with proper data, call invoice.update_invoice.
      invoice_request: Callable[
          ["Order", "Invoice", Union[str, NoneType], Any], Optional["Invoice"]
      ]
  
      #  Trigger after invoice is sent.
      invoice_sent: Callable[["Invoice", str, Any], Any]
  
      list_payment_sources: Callable[[str, Any], List["CustomerSource"]]
  
      #  Handle notification request.
      #
      #  Overwrite this method if the plugin is responsible for sending notifications.
      notify: Callable[["NotifyEventType", dict, Any], Any]
  
      #  Trigger when order is cancelled.
      #
      #  Overwrite this method if you need to trigger specific logic when an order is
      #  canceled.
      order_cancelled: Callable[["Order", Any], Any]
  
      #  Trigger when order is confirmed by staff.
      #
      #  Overwrite this method if you need to trigger specific logic after an order is
      #  confirmed.
      order_confirmed: Callable[["Order", Any], Any]
  
      #  Trigger when order is created.
      #
      #  Overwrite this method if you need to trigger specific logic after an order is
      #  created.
      order_created: Callable[["Order", Any], Any]
  
      #  Trigger when order is fulfilled.
      #
      #  Overwrite this method if you need to trigger specific logic when an order is
      #  fulfilled.
      order_fulfilled: Callable[["Order", Any], Any]
  
      #  Trigger when order is fully paid.
      #
      #  Overwrite this method if you need to trigger specific logic when an order is
      #  fully paid.
      order_fully_paid: Callable[["Order", Any], Any]
  
      #  Trigger when order is updated.
      #
      #  Overwrite this method if you need to trigger specific logic when an order is
      #  changed.
      order_updated: Callable[["Order", Any], Any]
  
      #  Trigger when page is created.
      #
      #  Overwrite this method if you need to trigger specific logic when a page is
      #  created.
      page_created: Callable[["Page", Any], Any]
  
      #  Trigger when page is deleted.
      #
      #  Overwrite this method if you need to trigger specific logic when a page is
      #  deleted.
      page_deleted: Callable[["Page", Any], Any]
  
      #  Trigger when page is updated.
      #
      #  Overwrite this method if you need to trigger specific logic when a page is
      #  updated.
      page_updated: Callable[["Page", Any], Any]
  
      #  Trigger directly before order creation.
      #
      #  Overwrite this method if you need to trigger specific logic before an order is
      #  created.
      preprocess_order_creation: Callable[
          [
              "CheckoutInfo",
              List["DiscountInfo"],
              Union[Iterable["CheckoutLineInfo"], NoneType],
              Any,
          ],
          Any,
      ]
  
      process_payment: Callable[["PaymentData", Any], "GatewayResponse"]
  
      #  Trigger when product is created.
      #
      #  Overwrite this method if you need to trigger specific logic after a product is
      #  created.
      product_created: Callable[["Product", Any], Any]
  
      #  Trigger when product is deleted.
      #
      #  Overwrite this method if you need to trigger specific logic after a product is
      #  deleted.
      product_deleted: Callable[["Product", List[int], Any], Any]
  
      #  Trigger when product is updated.
      #
      #  Overwrite this method if you need to trigger specific logic after a product is
      #  updated.
      product_updated: Callable[["Product", Any], Any]
  
      #  Trigger when product variant is created.
      #
      #  Overwrite this method if you need to trigger specific logic after a product
      #  variant is created.
      product_variant_created: Callable[["ProductVariant", Any], Any]
  
      #  Trigger when product variant is deleted.
      #
      #  Overwrite this method if you need to trigger specific logic after a product
      #  variant is deleted.
      product_variant_deleted: Callable[["ProductVariant", Any], Any]
  
      #  Trigger when product variant is updated.
      #
      #  Overwrite this method if you need to trigger specific logic after a product
      #  variant is updated.
      product_variant_updated: Callable[["ProductVariant", Any], Any]
  
      refund_payment: Callable[["PaymentData", Any], GatewayResponse]
  
      #  Define if storefront should add info about taxes to the price.
      #
      #  It is used only by the old storefront. The returned value determines if
      #  storefront should append info to the price about "including/excluding X% VAT".
      show_taxes_on_storefront: Callable[[bool], bool]
  
      #  Trigger when tracking number is updated.
      tracking_number_updated: Callable[["Fulfillment", Any], Any]
  
      void_payment: Callable[["PaymentData", Any], GatewayResponse]
  
      #  Handle received http request.
      #
      #  Overwrite this method if the plugin expects the incoming requests.
      webhook: Callable[[WSGIRequest, str, Any], HttpResponse]
  
      def token_is_required_as_payment_input(self, previous_value):
          return previous_value
  
      def get_payment_gateways(
          self, currency: Optional[str], checkout: Optional["Checkout"], previous_value
      ) -> List["PaymentGateway"]:
          payment_config = self.get_payment_config(previous_value)  # type: ignore
          payment_config = payment_config if payment_config != NotImplemented else []
          currencies = self.get_supported_currencies(previous_value=[])  # type: ignore
          currencies = currencies if currencies != NotImplemented else []
          if currency and currency not in currencies:
              return []
          gateway = PaymentGateway(
              id=self.PLUGIN_ID,
              name=self.PLUGIN_NAME,
              config=payment_config,
              currencies=currencies,
          )
          return [gateway]
  
      @classmethod
      def _update_config_items(
          cls, configuration_to_update: List[dict], current_config: List[dict]
      ):
          config_structure: dict = (
              cls.CONFIG_STRUCTURE if cls.CONFIG_STRUCTURE is not None else {}
          )
          for config_item in current_config:
              for config_item_to_update in configuration_to_update:
                  config_item_name = config_item_to_update.get("name")
                  if config_item["name"] == config_item_name:
                      new_value = config_item_to_update.get("value")
                      item_type = config_structure.get(config_item_name, {}).get("type")
                      if (
                          item_type == ConfigurationTypeField.BOOLEAN
                          and new_value
                          and not isinstance(new_value, bool)
                      ):
                          new_value = new_value.lower() == "true"
                      if item_type == ConfigurationTypeField.OUTPUT:
                          # OUTPUT field is read only. No need to update it
                          continue
                      config_item.update([("value", new_value)])
  
          # Get new keys that don't exist in current_config and extend it.
          current_config_keys = set(c_field["name"] for c_field in current_config)
          configuration_to_update_dict = {
              c_field["name"]: c_field["value"] for c_field in configuration_to_update
          }
          missing_keys = set(configuration_to_update_dict.keys()) - current_config_keys
          for missing_key in missing_keys:
              if not config_structure.get(missing_key):
                  continue
              current_config.append(
                  {
                      "name": missing_key,
                      "value": configuration_to_update_dict[missing_key],
                  }
              )
  
      @classmethod
      def validate_plugin_configuration(cls, plugin_configuration: "PluginConfiguration"):
          """Validate if provided configuration is correct.
  
          Raise django.core.exceptions.ValidationError otherwise.
          """
          return
  
      @classmethod
      def pre_save_plugin_configuration(cls, plugin_configuration: "PluginConfiguration"):
          """Trigger before plugin configuration will be saved.
  
          Overwrite this method if you need to trigger specific logic before saving a
          plugin configuration.
          """
  
      @classmethod
      def save_plugin_configuration(
          cls, plugin_configuration: "PluginConfiguration", cleaned_data
      ):
          current_config = plugin_configuration.configuration
          configuration_to_update = cleaned_data.get("configuration")
          if configuration_to_update:
              cls._update_config_items(configuration_to_update, current_config)
          if "active" in cleaned_data:
              plugin_configuration.active = cleaned_data["active"]
          cls.validate_plugin_configuration(plugin_configuration)
          cls.pre_save_plugin_configuration(plugin_configuration)
          plugin_configuration.save()
          if plugin_configuration.configuration:
              # Let's add a translated descriptions and labels
              cls._append_config_structure(plugin_configuration.configuration)
          return plugin_configuration
  
      @classmethod
      def _append_config_structure(cls, configuration: PluginConfigurationType):
          """Append configuration structure to config from the database.
  
          Database stores "key: value" pairs, the definition of fields should be declared
          inside of the plugin. Based on this, the plugin will generate a structure of
          configuration with current values and provide access to it via API.
          """
          config_structure = getattr(cls, "CONFIG_STRUCTURE") or {}
          fields_without_structure = []
          for configuration_field in configuration:
  
              structure_to_add = config_structure.get(configuration_field.get("name"))
              if structure_to_add:
                  configuration_field.update(structure_to_add)
              else:
                  fields_without_structure.append(configuration_field)
  
          if fields_without_structure:
              [
                  configuration.remove(field)  # type: ignore
                  for field in fields_without_structure
              ]
  
      @classmethod
      def _update_configuration_structure(cls, configuration: PluginConfigurationType):
          updated_configuration = []
          config_structure = getattr(cls, "CONFIG_STRUCTURE") or {}
          desired_config_keys = set(config_structure.keys())
          for config_field in configuration:
              if config_field["name"] not in desired_config_keys:
                  continue
              updated_configuration.append(copy(config_field))
  
          configured_keys = set(d["name"] for d in updated_configuration)
          missing_keys = desired_config_keys - configured_keys
  
          if not missing_keys:
              return updated_configuration
  
          default_config = cls.DEFAULT_CONFIGURATION
          if not default_config:
              return updated_configuration
  
          update_values = [copy(k) for k in default_config if k["name"] in missing_keys]
          if update_values:
              updated_configuration.extend(update_values)
          return updated_configuration
  
      @classmethod
      def get_default_active(cls):
          return cls.DEFAULT_ACTIVE
  
      def get_plugin_configuration(
          self, configuration: PluginConfigurationType
      ) -> PluginConfigurationType:
          if not configuration:
              configuration = []
          configuration = self._update_configuration_structure(configuration)
          if configuration:
              # Let's add a translated descriptions and labels
              self._append_config_structure(configuration)
          return configuration
  
      def excluded_shipping_methods_for_order(
          self,
          order: "Order",
          available_shipping_methods: List[ShippingMethod],
          previous_value,
      ) -> List[ExcludedShippingMethod]:
          return NotImplemented
  
      def excluded_shipping_methods_for_checkout(
          self,
          checkout: "Checkout",
          available_shipping_methods: List[ShippingMethod],
          previous_value,
      ) -> List[ExcludedShippingMethod]:
          return NotImplemented
  
      def is_event_active(self, event: str, channel=Optional[str]):
          return hasattr(self, event)
  