import math
from typing import Tuple

from django.contrib.auth.models import Group, User
from django.contrib.humanize.templatetags.humanize import intcomma
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import Error, models
from django.utils.translation import gettext as _
from esi.errors import TokenError, TokenExpiredError, TokenInvalidError
from esi.models import Token
from esi.openapi_clients import ESIClientProvider
from eveuniverse.models import (
    EveEntity,
    EveGroup,
    EveMarketGroup,
    EveSolarSystem,
    EveType,
)

from allianceauth.authentication.models import CharacterOwnership, State

# Create your models here.
from allianceauth.eveonline.models import EveCorporationInfo
from allianceauth.services.hooks import get_extension_logger

from buybackprogram.notification import (
    send_message_to_discord_channel,
    send_user_notification,
)

from .app_settings import (
    BUYBACKPROGRAM_TRACK_PREFILL_CONTRACTS,
    BUYBACKPROGRAM_TRACKING_PREFILL,
    get_site_url,
)
from .decorators import fetch_token_for_owner

logger = get_extension_logger(__name__)

esi = ESIClientProvider(
    compatibility_date="2025-07-23",
    ua_appname="MyProject",
    ua_version="0.0.1a1",
    operations=[
        "GetCharactersCharacterIdContracts",
        "GetCorporationsCorporationIdContracts",
        "GetUniverseStructuresStructureId",
        "GetCharactersCharacterIdContractsContractIdItems",
        "GetCorporationsCorporationIdContractsContractIdItems",
        "GetCorporationsCorporationIdWallets",
        "GetCorporationsCorporationIdDivisions",
        "GetCorporationsCorporationIdAssets",
    ],
)


def get_sentinel_user():
    """
    get user or create one
    :return:
    """

    return User.objects.get_or_create(username="deleted")[0]


class General(models.Model):
    """Meta model for app permissions"""

    class Meta:
        managed = False
        default_permissions = ()
        permissions = (
            ("basic_access", "Can access this app and see own statics."),
            (
                "manage_programs",
                "Can manage own buyback programs and see own program statics.",
            ),
            ("see_leaderboard", "Can see leaderboards for all available programs."),
            ("see_performance", "Can see performance for all available programs."),
            ("see_all_statics", "Can see all program statics."),
            ("manage_all_programs", "Can manage ALL buyback programs"),
        )


class Owner(models.Model):
    """A corporation that has buyback programs"""

    ERROR_NONE = 0
    ERROR_TOKEN_INVALID = 1
    ERROR_TOKEN_EXPIRED = 2
    ERROR_NO_CHARACTER = 3
    ERROR_INSUFFICIENT_PERMISSIONS = 4
    ERROR_ESI_UNAVAILABLE = 5

    ERRORS_LIST = [
        (ERROR_NONE, "No error"),
        (ERROR_TOKEN_INVALID, "Invalid token"),
        (ERROR_TOKEN_EXPIRED, "Expired token"),
        (ERROR_NO_CHARACTER, "No character configured"),
        (ERROR_INSUFFICIENT_PERMISSIONS, "Insufficient permissions"),
        (ERROR_ESI_UNAVAILABLE, "ESI API is currently unavailable"),
    ]

    corporation = models.ForeignKey(
        EveCorporationInfo,
        on_delete=models.deletion.CASCADE,
        related_name="+",
    )
    character = models.ForeignKey(
        CharacterOwnership,
        help_text="Character used for retrieving info",
        on_delete=models.deletion.PROTECT,
        related_name="+",
    )

    user = models.ForeignKey(
        User,
        help_text="User that manages the program",
        on_delete=models.deletion.PROTECT,
        related_name="+",
    )

    class Meta:
        default_permissions = ()

    @fetch_token_for_owner(
        [
            "esi-contracts.read_character_contracts.v1",
            "esi-contracts.read_corporation_contracts.v1",
            "esi-universe.read_structures.v1",
            "esi-wallet.read_corporation_wallets.v1",
        ]
    )
    @fetch_token_for_owner(["esi-wallet.read_corporation_wallets.v1"])
    def update_wallets_esi(self, *args, **kwargs):
        """Fetches wallet names and balances from ESI and updates the cache."""

        token = kwargs.get("token")
        if not token:
            for arg in args:
                if hasattr(arg, "valid_access_token"):
                    token = arg
                    break

        if not token:
            logger.error(
                f"Failed to find ESI Token in arguments for {self.corporation}"
            )
            return

        logger.debug(f"Updating wallets for {self.corporation}")

        try:
            names_dict = {}

            # 1. Fetch Custom Names (Graceful fallback if scope is missing)
            try:
                divisions_res = (
                    esi.client.Corporation.GetCorporationsCorporationIdDivisions(
                        corporation_id=self.corporation.corporation_id, token=token
                    ).results()
                )

                # Handle both object and dict formats returned from ESI
                try:
                    wallets = divisions_res.wallet
                except AttributeError:
                    try:
                        wallets = divisions_res.model_dump().get("wallet", [])
                    except AttributeError:
                        wallets = []

                for w in wallets:
                    try:
                        div = w.division
                        name = w.name
                    except AttributeError:
                        div = w["division"]
                        name = w["name"]
                    names_dict[div] = name

            except Exception as e:
                # Catch 403 Forbidden (missing scope) and fallback to default names
                logger.debug(
                    f"Skipping custom wallet names for {self.corporation} (likely missing read_divisions scope): {e}"
                )

            # 2. Fetch Balances
            balances_res = esi.client.Wallet.GetCorporationsCorporationIdWallets(
                corporation_id=self.corporation.corporation_id, token=token
            ).results()

            # 3. Update Database Cache
            for wallet in balances_res:
                try:
                    div = wallet.division
                    bal = wallet.balance
                except AttributeError:
                    div = wallet["division"]
                    bal = wallet["balance"]

                # Use default EVE name if custom fetch failed
                default_name = "Master Wallet" if div == 1 else f"Division {div}"
                name = names_dict.get(div, default_name)

                WalletDivision.objects.update_or_create(
                    owner=self, division_id=div, defaults={"name": name, "balance": bal}
                )
            logger.debug(f"Successfully updated wallets for {self.corporation}")

        except TokenError:
            logger.warning(
                f"Skipping wallet sync for {self.corporation}: Missing token or required scopes. "
                "Manager needs to re-authenticate."
            )
        except Exception as e:
            logger.error(f"Failed to update wallets for {self.corporation}: {e}")

    def update_contracts_esi(self, token, all_contracts=None):
        logger.debug("Processing normal contracts for %s" % self.character)

        if all_contracts is None:
            contracts = self._fetch_contracts(token=token)
            logger.debug("Got %s character contracts" % len(contracts))
            logger.debug("Fetching corporation contracts for %s" % self.corporation)
            corporation_contracts = self._fetch_corporation_contracts(token=token)
            logger.debug("Got %s corporation contracts" % len(corporation_contracts))
            all_contracts = contracts + corporation_contracts

        tracked_contrats = list()

        logger.debug("Total contracts received: %s" % len(all_contracts))

        # Get all tracking objects from the database
        tracking_numbers = Tracking.objects.all()

        logger.debug("Got %s tracking numbers from database" % len(tracking_numbers))

        # Start looping for all stored tracking objects
        for tracking in tracking_numbers:
            # If the tracking has an active program (not deleted)
            if tracking.program:
                # Start checking if we find any matches from our ESI contracts
                for contract in all_contracts:
                    # Only get contracts with the correct prefill ticker
                    if tracking.tracking_number in contract["title"]:
                        logger.debug(
                            "Found matching tracking for: %s, starting processing"
                            % tracking.tracking_number
                        )
                        self._process_contract(contract, tracking, token)

                        tracked_contrats.append(contract)

                        break  # If we have found a match from our ESI contracts wi will stop looping on the contract

        # Check for possible scam contracts that are pretending to be buyback contracts

        if BUYBACKPROGRAM_TRACK_PREFILL_CONTRACTS:
            logger.debug("Starting untracked contracts check")

            # Check what contracts we already have processed earlier
            untracked_contracts = [
                x for x in all_contracts if x not in tracked_contrats
            ]

            logger.debug(
                "Found %s untracked contracts out of %s"
                % (len(untracked_contracts), len(all_contracts))
            )

            for contract in untracked_contracts:
                # Skip contracts that are already finished or rejected
                if contract.get("status") in ["finished", "deleted", "rejected"]:
                    continue
                if BUYBACKPROGRAM_TRACKING_PREFILL in contract["title"]:
                    try:
                        tracking = Tracking.objects.get(
                            tracking_number__contains=contract["title"]
                        )
                        logger.debug(
                            "Contract %s already tracked, passing"
                            % contract["contract_id"]
                        )

                    except Tracking.DoesNotExist:
                        # Check if it matches a reverse tracking — skip it if so
                        if ReverseTracking.objects.filter(
                            tracking_number__contains=contract["title"]
                        ).exists():
                            logger.debug(
                                "Contract %s matches a reverse tracking, skipping"
                                % contract["contract_id"]
                            )
                            continue

                        logger.debug(
                            "Contract %s is not tracked, starting updates"
                            % contract["contract_id"]
                        )
                        self._process_contract_without_tracking(contract, token)
        else:
            logger.debug(
                "Track prefill contracts is set to %s, passing prefill contract checks"
                % BUYBACKPROGRAM_TRACK_PREFILL_CONTRACTS
            )

    def update_reverse_contracts_esi(self, token, all_contracts=None):
        logger.debug("Processing reverse contracts for %s" % self.character)

        if all_contracts is None:
            contracts = self._fetch_contracts(token=token)
            corporation_contracts = self._fetch_corporation_contracts(token=token)
            all_contracts = contracts + corporation_contracts

        logger.debug("Total contracts for reverse matching: %s" % len(all_contracts))

        reverse_trackings = ReverseTracking.objects.all()

        logger.debug("Got %s reverse tracking numbers" % len(reverse_trackings))

        for tracking in reverse_trackings:
            if tracking.program:
                for contract in all_contracts:
                    if tracking.tracking_number in contract["title"]:
                        logger.debug(
                            "Found matching reverse tracking for: %s"
                            % tracking.tracking_number
                        )
                        self._process_reverse_contract(contract, tracking, token)
                        break

    def update_all_contracts_esi(self, token):
        """Fetch contracts ONCE then process both normal and reverse trackings."""
        logger.debug("Fetching all contracts for %s" % self.character)

        contracts = self._fetch_contracts(token=token)
        corporation_contracts = self._fetch_corporation_contracts(token=token)
        all_contracts = contracts + corporation_contracts

        logger.debug("Total contracts fetched: %s" % len(all_contracts))

        self.update_contracts_esi(token=token, all_contracts=all_contracts)
        self.update_reverse_contracts_esi(token=token, all_contracts=all_contracts)

    def _process_reverse_contract(self, contract, tracking, token):
        """Links an ESI contract to a ReverseTracking object and fetches its items."""
        try:
            contract_obj = Contract.objects.get(contract_id=contract["contract_id"])
            is_new_contract = False
        except Contract.DoesNotExist:
            contract_obj = Contract()
            is_new_contract = True

        contract_obj.contract_id = contract["contract_id"]
        contract_obj.issuer_id = contract.get("issuer_id")
        contract_obj.assignee_id = contract.get("assignee_id")
        contract_obj.availability = contract.get("availability", "public")
        contract_obj.for_corporation = contract.get("for_corporation", False)
        contract_obj.issuer_corporation_id = contract.get("issuer_corporation_id", 0)
        contract_obj.status = contract.get("status")
        contract_obj.title = contract.get("title")
        contract_obj.price = contract.get("reward") or contract.get("price", 0)
        contract_obj.date_issued = contract.get("date_issued")
        contract_obj.date_expired = contract.get("date_expired")
        contract_obj.date_completed = contract.get("date_completed")
        contract_obj.start_location_id = contract.get("start_location_id")
        contract_obj.volume = contract.get("volume", 0)

        try:
            contract_obj.save()
            logger.debug("Saved reverse contract %s" % contract_obj.contract_id)
        except Exception as e:
            logger.error("Error saving reverse contract: %s" % e)
            return

        ReverseTracking.objects.filter(pk=tracking.pk).update(contract=contract_obj)

        logger.debug(
            "Linked reverse contract %s to tracking %s"
            % (contract_obj.contract_id, tracking.tracking_number)
        )

        # Fetch contract items (what the player put in "I will receive")
        character_id = self.character.character.character_id
        corporation_id = self.character.character.corporation_id

        try:
            if not contract.get("is_corporation"):
                raw_items = esi.client.Contracts.GetCharactersCharacterIdContractsContractIdItems(
                    character_id=character_id,
                    contract_id=contract["contract_id"],
                    token=token,
                ).results()
            else:
                raw_items = esi.client.Contracts.GetCorporationsCorporationIdContractsContractIdItems(
                    corporation_id=corporation_id,
                    contract_id=contract["contract_id"],
                    token=token,
                ).results()

            contract_items = []
            for item in raw_items:
                try:
                    contract_items.append(dict(item))
                except (TypeError, AttributeError):
                    contract_items.append(item.model_dump())

        except Exception as e:
            if "304" in str(e) or "HTTPNotModified" in str(e):
                logger.debug(
                    f"ESI returned 304 for items in reverse contract {contract['contract_id']}. Skipping."
                )
            else:
                logger.error(
                    f"Error fetching items for reverse contract {contract['contract_id']}: {e}"
                )
            contract_items = []

        # Create ContractItem records so notifications can compare them
        if contract_items:
            ContractItem.objects.filter(contract=contract_obj).delete()
            objs = []
            for item_data in contract_items:
                try:
                    itm, _ = EveType.objects.get_or_create_esi(id=item_data["type_id"])
                    objs.append(
                        ContractItem(
                            contract=contract_obj,
                            eve_type=itm,
                            quantity=item_data["quantity"],
                        )
                    )
                except Exception as e:
                    logger.error(
                        f"Error creating ContractItem for reverse contract: {e}"
                    )

            try:
                ContractItem.objects.bulk_create(objs)
                logger.debug(
                    "Created %s items for reverse contract %s"
                    % (len(objs), contract_obj.contract_id)
                )
            except Error as e:
                logger.error(
                    "Error bulk creating items for reverse contract %s: %s"
                    % (contract_obj.contract_id, e)
                )

        if is_new_contract:
            self._set_reverse_contract_notifications(tracking, contract_obj)

            # Discord notifications
            notifications = ContractNotification.objects.filter(contract=contract_obj)
            notes = str()
            if notifications:
                for note in notifications:
                    notes += str(note.message)
                    notes += "\n\n"

            assigned_from = EveEntity.objects.resolve_name(contract_obj.issuer_id)

            if tracking.program.is_corporation:
                assigned_to = str(tracking.program.owner.corporation)
            else:
                assigned_to = tracking.program.owner.character.character.character_name

            reverse_items = ReverseTrackingItem.objects.filter(
                tracking=tracking
            ).select_related("eve_type")
            contract_items_str = "\n".join(
                f"{i.eve_type.name} x {i.quantity}" for i in reverse_items
            )

            user_message = {
                "contract": contract_obj,
                "tracking": tracking,
                "notes": notes,
                "title": "New reverse buyback request for program {0}".format(
                    tracking.program.name
                ),
                "color": 0x5BC0DE,
                "value": intcomma(int(contract_obj.price)),
                "assigned_from": assigned_from,
                "assigned_to": assigned_to,
                "contract_items": contract_items_str,
                "is_reverse": True,
            }

            if tracking.program.discord_dm_notification:
                send_user_notification(
                    user=self.user,
                    level="success",
                    message=user_message,
                )
            else:
                logger.debug(
                    "Reverse program owner does not want DM notifications, passing"
                )

            if tracking.program.discord_channel_notification:
                logger.debug(
                    "Reverse program wants channel notification, attempting to send via webhook"
                )
                send_message_to_discord_channel(
                    webhook=tracking.program.discord_channel_notification,
                    message=user_message,
                )
            else:
                logger.debug(
                    "Reverse program owner does not want channel notifications, passing"
                )

    def update_hangar_divisions_esi(self, token):
        """Fetches corporation hangar division names from ESI and updates the cache."""
        logger.debug(f"Updating hangar division names for {self.corporation}")

        try:
            divisions_res = (
                esi.client.Corporation.GetCorporationsCorporationIdDivisions(
                    corporation_id=self.corporation.corporation_id, token=token
                ).results()
            )

            # .results() returns a list wrapping the response object — unwrap it
            if isinstance(divisions_res, list):
                divisions_res = divisions_res[0] if divisions_res else None

            if not divisions_res:
                logger.debug(f"No division data returned for {self.corporation}")
                return

            try:
                hangars = divisions_res.hangar
            except AttributeError:
                try:
                    hangars = divisions_res.model_dump().get("hangar", [])
                except AttributeError:
                    hangars = []

            for h in hangars:
                try:
                    div = h.division
                    name = getattr(h, "name", None)
                except AttributeError:
                    if isinstance(h, dict):
                        div = h.get("division")
                        name = h.get("name")
                    else:
                        continue

                if not div:
                    continue

                DIVISION_DEFAULTS = {
                    1: "1st Division",
                    2: "2nd Division",
                    3: "3rd Division",
                    4: "4th Division",
                    5: "5th Division",
                    6: "6th Division",
                    7: "7th Division",
                }
                default_name = DIVISION_DEFAULTS.get(div, f"Division {div}")
                HangarDivision.objects.update_or_create(
                    owner=self,
                    division_id=div,
                    defaults={"name": name if name else default_name},
                )

            logger.debug(
                f"Successfully updated hangar division names for {self.corporation}"
            )

        except Exception as e:
            if "HTTPNotModified" in str(type(e)) or "304" in str(e):
                if not HangarDivision.objects.filter(owner=self).exists():
                    from django.core.cache import cache

                    cache.clear()
                    logger.debug(
                        f"Hangar divisions for {self.corporation} returned 304 but DB is empty — cleared cache"
                    )
                else:
                    logger.debug(
                        f"Hangar divisions for {self.corporation} not modified (304). Skipping."
                    )
            else:
                logger.error(
                    f"Failed to update hangar divisions for {self.corporation}: {e}"
                )

    def update_hangar_esi(self, token):
        """Fetches corporation assets from ESI and updates HangarStock for all reverse programs."""
        logger.debug("Fetching hangar assets for %s" % self.corporation)

        reverse_programs = ReverseProgram.objects.filter(owner=self)

        if not reverse_programs.exists():
            logger.debug(
                "No reverse programs for %s, skipping hangar sync" % self.corporation
            )
            return

        try:
            assets = esi.client.Assets.GetCorporationsCorporationIdAssets(
                corporation_id=self.corporation.corporation_id,
                token=token,
            ).results()
        except Exception as e:
            if "304" in str(e) or "HTTPNotModified" in str(e):
                logger.debug(
                    "Assets for %s not modified (304). Skipping." % self.corporation
                )
            else:
                logger.error("Error fetching assets for %s: %s" % (self.corporation, e))
            return

        # Only include items sitting directly in a hangar division
        # Items in containers/ships/space use different flags
        HANGAR_FLAGS = {
            "Hangar",
            "CorpSAG1",
            "CorpSAG2",
            "CorpSAG3",
            "CorpSAG4",
            "CorpSAG5",
            "CorpSAG6",
        }

        # Build a map of {(location_id, location_flag): {type_id: quantity}}
        location_stock = {}
        for asset in assets:
            try:
                location_id = asset.location_id
                type_id = asset.type_id
                quantity = asset.quantity
                location_flag = asset.location_flag
            except AttributeError:
                location_id = asset["location_id"]
                type_id = asset["type_id"]
                quantity = asset["quantity"]
                location_flag = asset["location_flag"]

            # Skip items not directly in a hangar division
            if location_flag not in HANGAR_FLAGS:
                continue

            key = (location_id, location_flag)
            if key not in location_stock:
                location_stock[key] = {}
            location_stock[key][type_id] = (
                location_stock[key].get(type_id, 0) + quantity
            )

        # Update HangarStock for each reverse program
        for program in reverse_programs:
            program_location_ids = set()
            for loc in program.location.all():
                if loc.asset_location_id:
                    program_location_ids.add(loc.asset_location_id)
                elif loc.structure_id:
                    program_location_ids.add(loc.structure_id)

            division_flag = (
                program.hangar_division.location_flag
                if program.hangar_division
                else "Hangar"
            )

            logger.debug(
                "Asset location keys found: %s" % list(location_stock.keys())[:20]
            )
            logger.debug(
                "Program location IDs: %s, division flag: %s"
                % (program_location_ids, division_flag)
            )

            # Auto-discover asset_location_id if not set and structure_id has no match
            for loc in program.location.all():
                if not loc.asset_location_id and loc.structure_id:
                    if (loc.structure_id, division_flag) not in location_stock:
                        # Look for any location with the correct division flag
                        # that is close to the structure_id (same ID range)
                        for asset_loc_id, asset_flag in location_stock.keys():
                            if asset_flag == division_flag:
                                logger.debug(
                                    "Auto-discovered asset_location_id %s for structure %s"
                                    % (asset_loc_id, loc.structure_id)
                                )
                                loc.asset_location_id = asset_loc_id
                                loc.save()
                                program_location_ids.discard(loc.structure_id)
                                program_location_ids.add(asset_loc_id)
                                break

            # Merge stock across all matching locations for this program
            merged_stock = {}
            for loc_id in program_location_ids:
                key = (loc_id, division_flag)
                for type_id, qty in location_stock.get(key, {}).items():
                    merged_stock[type_id] = merged_stock.get(type_id, 0) + qty

            # Clear existing stock and replace
            HangarStock.objects.filter(program=program).delete()

            objs = []
            for type_id, quantity in merged_stock.items():
                eve_type = EveType.objects.filter(pk=type_id).first()
                if eve_type:
                    objs.append(
                        HangarStock(
                            program=program,
                            eve_type=eve_type,
                            quantity=quantity,
                        )
                    )

            try:
                HangarStock.objects.bulk_create(objs)
                logger.debug(
                    "Updated %s hangar stock items for program %s"
                    % (len(objs), program.pk)
                )
            except Exception as e:
                logger.error(
                    "Error saving hangar stock for program %s: %s" % (program.pk, e)
                )

    def _process_contract(self, contract, tracking, token):
        # Check if we already have the contract stored
        try:
            old_contract = Contract.objects.get(contract_id=contract["contract_id"])

            logger.debug(
                "Contract %s is already stored in database" % contract["contract_id"]
            )

        except Contract.DoesNotExist:
            logger.debug(
                "No matching contracts stored for %s in database, new contract."
                % contract["contract_id"]
            )
            old_contract = Contract.objects.none()
            old_contract.status = False

        logger.debug("User has token: %s" % contract["start_location_id"])

        # If we have found a contract from database that is not yet finished
        if old_contract.status not in ["finished", "rejected"]:
            logger.debug(
                "Contract %s status is still pending, starting updates"
                % contract["contract_id"]
            )

            # Get location name for contract
            contract_location_name = self._get_location_name(
                token, contract["start_location_id"]
            )

            logger.debug("Got contract location name: %s" % contract_location_name)

            # Create or update the found contract
            obj, created = Contract.objects.update_or_create(
                contract_id=contract["contract_id"],
                defaults={
                    "contract_id": contract["contract_id"],
                    "assignee_id": contract["assignee_id"],
                    "availability": contract["availability"],
                    "date_completed": contract["date_completed"],
                    "date_expired": contract["date_expired"],
                    "date_issued": contract["date_issued"],
                    "for_corporation": contract["for_corporation"],
                    "issuer_corporation_id": contract["issuer_corporation_id"],
                    "issuer_id": contract["issuer_id"],
                    "start_location_id": contract["start_location_id"],
                    "location_name": contract_location_name,
                    "price": contract["price"],
                    "status": contract["status"],
                    "title": contract["title"],
                    "volume": contract["volume"],
                    "no_tracking": False,
                },
            )

            # If we have created a new contract
            if created:
                logger.debug(
                    "Contract %s created, linking tracking object %s"
                    % (
                        contract["contract_id"],
                        tracking.tracking_number,
                    )
                )

                try:
                    Tracking.objects.filter(pk=tracking.id).update(contract=obj)
                except Error as e:
                    logger.error(
                        "Error linking contract %s with tracking %s: %s"
                        % (
                            contract["contract_id"],
                            tracking.tracking_number,
                            e,
                        )
                    )

                logger.debug(
                    "New contract %s has been created. Starting item fetch"
                    % contract["contract_id"]
                )

                character_id = self.character.character.character_id

                corporation_id = self.character.character.corporation_id

                logger.debug(
                    "Fetching items for %s with character %s. Corporation contract: %s"
                    % (
                        contract["contract_id"],
                        character_id,
                        contract["is_corporation"],
                    )
                )

                try:
                    if not contract["is_corporation"]:
                        raw_items = esi.client.Contracts.GetCharactersCharacterIdContractsContractIdItems(
                            character_id=character_id,
                            contract_id=contract["contract_id"],
                            token=token,
                        ).results()
                    else:
                        raw_items = esi.client.Contracts.GetCorporationsCorporationIdContractsContractIdItems(
                            corporation_id=corporation_id,
                            contract_id=contract["contract_id"],
                            token=token,
                        ).results()

                    # Convert the objects to a list of dictionaries safely
                    contract_items = []
                    for item in raw_items:
                        try:
                            contract_items.append(dict(item))
                        except (TypeError, AttributeError):
                            contract_items.append(item.model_dump())

                except Exception as e:
                    if "304" in str(e) or "HTTPNotModified" in str(e):
                        logger.debug(
                            f"ESI returned 304 for items in contract {contract['contract_id']}. Skipping."
                        )
                        contract_items = []
                    else:
                        logger.error(
                            f"Fatal error fetching items for {contract['contract_id']}: {e}"
                        )
                        raise e

                logger.debug(
                    "%s items found in contract %s"
                    % (len(contract_items), contract["contract_id"])
                )

                corporations = CharacterOwnership.objects.filter(
                    user=tracking.program.owner.user
                ).values_list("character__corporation_id", flat=True)

                # Prepare objects for bulk create (using the new contract_items list of dicts)
                objs = []
                items = []
                for item_data in contract_items:  # Use item_data which is now a dict
                    cont = Contract.objects.get(contract_id=contract["contract_id"])
                    itm, _ = EveType.objects.get_or_create_esi(id=item_data["type_id"])

                    contract_item = ContractItem(
                        contract=cont,
                        eve_type=itm,
                        quantity=item_data["quantity"],
                    )
                    objs.append(contract_item)
                    items.append(f"{itm.name} x {item_data['quantity']}")

                try:
                    ContractItem.objects.bulk_create(objs)
                    logger.debug(
                        "Succesfully added %s items for contract %s into database"
                        % (len(objs), contract["contract_id"])
                    )
                except Error as e:
                    logger.error(
                        "Error adding items for contract %s: %s"
                        % (contract["contract_id"], e)
                    )

                # Check and see if any notifications/warnings should be set on the contract
                logger.debug(
                    "Starting contract notification check to see if we need to add any notification icons"
                )
                self._set_contract_notifications(
                    tracking, obj, corporations, tracking.program
                )

                self._set_watchlist_contract_notifications(obj, tracking.program)

                # Notifications for users who have the notifications enabled

                if not contract["is_corporation"]:
                    assigned_to = self.character.character.character_name
                else:
                    assigned_to = self.character.character.corporation_name

                notifications = ContractNotification.objects.filter(
                    contract__contract_id=contract["contract_id"]
                )

                notes = str()

                if notifications:
                    for note in notifications:
                        notes += str(note.message)
                        notes += "\n\n"

                logger.debug("Contract contains %s" % "\n".join(items))

                # Check if the program wants to display the item list
                if tracking.program.discord_show_item_list:
                    contract_item_list = "\n".join(items)
                else:
                    contract_item_list = ""

                contract_url = (
                    get_site_url()
                    + "/buybackprogram/tracking/"
                    + tracking.tracking_number
                )

                # Limit contract items to fit into embed limitations
                if (
                    len(contract_item_list) > 2000
                    or not tracking.program.discord_show_item_list
                ):
                    contract_item_list = (
                        contract_item_list
                        + "\n[See all contract items ...]("
                        + contract_url
                        + ")"
                    )

                user_message = {
                    "contract": obj,
                    "contract_items": contract_item_list,
                    "tracking": tracking,
                    "notes": notes,
                    "title": "New buyback contract assigned for program {0}".format(
                        tracking.program.name
                    ),
                    "color": 0x5BC0DE,
                    "value": intcomma(int(contract["price"])),
                    "assigned_to": assigned_to,
                    "assigned_from": EveEntity.objects.resolve_name(
                        contract["issuer_id"]
                    ),
                }

                # If tracking is active and we should send a message for our users
                if tracking.program.discord_dm_notification:
                    send_user_notification(
                        user=self.user,
                        level="success",
                        message=user_message,
                    )
                else:
                    logger.debug(
                        "Program owner does not want DM notifications, passing"
                    )

                # Notifications for the discord channel
                if tracking.program.discord_channel_notification:
                    logger.debug(
                        "Program wants channel notification, attempting to send via webhook"
                    )
                    send_message_to_discord_channel(
                        webhook=tracking.program.discord_channel_notification,
                        message=user_message,
                    )
                else:
                    logger.debug(
                        "Program owner does not want channel notifications, passing"
                    )

            # If contract was updated instead of created
            else:
                logger.debug(
                    "Contract %s is not new, updated the old contract."
                    % obj.contract_id
                )

            # Check if the contract status has changed from ongoing to finished.
            if old_contract.status == "outstanding" and obj.status in [
                "finished",
                "rejected",
            ]:
                logger.debug(
                    "Contract %s has been completed. Status has changed to %s"
                    % (obj.contract_id, obj.status)
                )

                # Get notification settings for the contract issuer
                user_settings = UserSettings.objects.get(user=tracking.issuer_user)

                # If user has not disabled notifications
                if user_settings.disable_notifications is False:
                    if not contract["is_corporation"]:
                        assigned_to = self.character.character.character_name
                    else:
                        assigned_to = self.character.character.corporation_name

                    # Check if the contract was accepted or rejected
                    if contract["status"] == "finished":
                        status = "accepted"
                        color = 0x5CB85C
                        level = "success"
                    elif contract["status"] == "rejected":
                        status = "rejected"
                        color = 0xD9534F
                        level = "danger"
                    else:
                        status = contract["status"]

                    notifications = ContractNotification.objects.filter(
                        contract__contract_id=contract["contract_id"]
                    )

                    notes = str()
                    items = []

                    if notifications:
                        for note in notifications:
                            notes += str(note.message)
                            notes += "\n\n"

                    user_message = {
                        "contract": obj,
                        "tracking": tracking,
                        "title": "Your buyback contract has been {0}".format(status),
                        "color": color,
                        "notes": notes,
                        "value": intcomma(int(contract["price"])),
                        "assigned_to": assigned_to,
                        "assigned_from": EveEntity.objects.resolve_name(
                            contract["issuer_id"]
                        ),
                    }

                    send_user_notification(
                        user=tracking.issuer_user,
                        level=level,
                        message=user_message,
                    )
                else:
                    logger.debug(
                        "Contract assigner has notifications set to %s, passing"
                        % user_settings.disable_notifications
                    )
            else:
                logger.debug(
                    "No changes to the status of contract %s, update completed"
                    % obj.contract_id
                )

    def _process_contract_without_tracking(self, contract, token):
        # Check if we already have the contract stored
        try:
            old_contract = Contract.objects.get(contract_id=contract["contract_id"])

            logger.debug(
                "Untracked contract %s is already stored in database"
                % contract["contract_id"]
            )

        except Contract.DoesNotExist:
            logger.debug(
                "No matching contracts stored for %s in database, new untracked contract."
                % contract["contract_id"]
            )
            old_contract = Contract.objects.none()
            old_contract.status = False

        logger.debug("User has token: %s" % contract["start_location_id"])

        # If we have found a contract from database that is not yet finished
        if old_contract.status not in ["finished", "rejected"]:
            logger.debug(
                "Untracked contract %s status is still pending, starting updates"
                % contract["contract_id"]
            )

            # Get location name for contract
            contract_location_name = self._get_location_name(
                token, contract["start_location_id"]
            )
            logger.debug("Got contract location name: %s" % contract_location_name)

            # Create or update the found contract
            obj, created = Contract.objects.update_or_create(
                contract_id=contract["contract_id"],
                defaults={
                    "contract_id": contract["contract_id"],
                    "assignee_id": contract["assignee_id"],
                    "availability": contract["availability"],
                    "date_completed": contract["date_completed"],
                    "date_expired": contract["date_expired"],
                    "date_issued": contract["date_issued"],
                    "for_corporation": contract["for_corporation"],
                    "issuer_corporation_id": contract["issuer_corporation_id"],
                    "issuer_id": contract["issuer_id"],
                    "start_location_id": contract["start_location_id"],
                    "location_name": contract_location_name,
                    "price": contract["price"],
                    "status": contract["status"],
                    "title": contract["title"],
                    "volume": contract["volume"],
                    "no_tracking": True,
                },
            )

            # If we have created a new contract
            if created:
                logger.debug(
                    "Contract %s created without tracking object, possible scam!"
                    % (contract["contract_id"],)
                )

                character_id = self.character.character.character_id

                corporation_id = self.character.character.corporation_id

                logger.debug(
                    "Fetching items for %s with character %s. Corporation contract: %s"
                    % (
                        contract["contract_id"],
                        character_id,
                        contract["is_corporation"],
                    )
                )

                try:
                    if not contract["is_corporation"]:
                        logger.debug(
                            "Looking up items for %s via character endpoint"
                            % contract["contract_id"]
                        )
                        raw_items = esi.client.Contracts.GetCharactersCharacterIdContractsContractIdItems(
                            character_id=character_id,
                            contract_id=contract["contract_id"],
                            token=token,
                        ).results()
                    else:
                        logger.debug(
                            "Looking up items for %s via corporation endpoint"
                            % contract["contract_id"]
                        )
                        raw_items = esi.client.Contracts.GetCorporationsCorporationIdContractsContractIdItems(
                            corporation_id=corporation_id,
                            contract_id=contract["contract_id"],
                            token=token,
                        ).results()

                    # Convert the Pydantic objects to a list of standard dictionaries safely
                    contract_items = []
                    for item in raw_items:
                        try:
                            contract_items.append(dict(item))
                        except (TypeError, AttributeError):
                            contract_items.append(item.model_dump())

                except Exception as e:
                    if "304" in str(e) or "HTTPNotModified" in str(e):
                        logger.debug(
                            f"ESI returned 304 for items in untracked contract {contract['contract_id']}. Skipping."
                        )
                        contract_items = []
                    else:
                        logger.error(
                            f"Fatal error fetching items for untracked contract {contract['contract_id']}: {e}"
                        )
                        contract_items = []

                logger.debug(
                    "%s items found in untracked contract %s"
                    % (len(contract_items), contract["contract_id"])
                )

                # Get the contract object once
                cont = Contract.objects.get(contract_id=contract["contract_id"])

                # Prevent endless loop / duplicate entries: delete existing items before saving new ones
                ContractItem.objects.filter(contract=cont).delete()

                objs = []

                # Prepare objects for bulk create using the new 'item_data' dictionary format
                for item_data in contract_items:
                    itm, _ = EveType.objects.get_or_create_esi(id=item_data["type_id"])

                    contract_item = ContractItem(
                        contract=cont,
                        eve_type=itm,
                        quantity=item_data["quantity"],
                    )
                    objs.append(contract_item)

                try:
                    if objs:  # Only attempt bulk create if we have actual items
                        ContractItem.objects.bulk_create(objs)
                        logger.debug(
                            "Succesfully added %s items for contract %s into database"
                            % (len(objs), contract["contract_id"])
                        )
                except Error as e:
                    logger.error(
                        "Error adding items for contract %s: %s"
                        % (contract["contract_id"], e)
                    )

                # Check and see if any notifications/warnings should be set on the contract
                self._set_suspicious_contract_notifications(obj)

            # If contract was updated instead of created
            else:
                logger.debug(
                    "Contract %s is not new, updated the old contract."
                    % obj.contract_id
                )

            # Check if the contract status has changed from ongoing to finished.
            if old_contract.status == "outstanding" and obj.status in [
                "finished",
                "rejected",
            ]:
                logger.debug(
                    "Contract %s has been completed. Status has changed to %s"
                    % (obj.contract_id, obj.status)
                )

            else:
                logger.debug(
                    "No changes to the status of contract %s, update completed"
                    % obj.contract_id
                )

    def _get_location_name(self, token, structid) -> str:
        logger.debug(f"Starting location name fetch for ID: {structid}")

        # 1. Attempt to get name from Cache first (Universal lookup)
        cache_obj, created = StructureNameCache.objects.get_or_create(
            structure_id=structid, defaults={"name": "Unknown"}
        )

        if cache_obj.name != "Unknown":
            return cache_obj.name

        # 2. Logic for NPC Stations
        if structid <= 100000000:
            resolved_name = EveEntity.objects.resolve_name(structid)
            # Update cache with NPC name
            cache_obj.name = resolved_name
            cache_obj.save()
            return resolved_name

        # 3. Logic for Player Structures (ESI Fetch)
        logger.debug(f"Name for structure {structid} is Unknown, fetching from ESI.")
        try:
            op = esi.client.Universe.GetUniverseStructuresStructureId(
                structure_id=structid, token=token
            )

            # Force Cache Bypass
            if not hasattr(op, "headers"):
                op.headers = {}
            op.headers["If-None-Match"] = ""
            op.headers["Cache-Control"] = "no-cache"

            raw_label = op.results()

            # Unpack list if ESI returned a list-wrapped object
            label = (
                raw_label[0]
                if isinstance(raw_label, list) and len(raw_label) > 0
                else raw_label
            )

            # Extract name safely
            name = getattr(label, "name", "Unknown")
            if name == "Unknown" and isinstance(label, dict):
                name = label.get("name", "Unknown")

            # Update cache if a valid name was found
            if name != "Unknown":
                cache_obj.name = name
                cache_obj.save()
                logger.debug(f"Successfully updated cache for {structid} to: {name}")
                return name

            return "Unknown"

        except Exception as ex:
            error_msg = str(ex)
            if "304" in error_msg or "HTTPNotModified" in error_msg:
                logger.debug(
                    f"ESI returned 304 for {structid}. Name remains 'Unknown'."
                )
            else:
                logger.error(f"Error fetching location info for {structid}: {ex}")

            return "Unknown"

    def _fetch_contracts(self, token) -> list:
        character_id = self.character.character.character_id
        contracts = []

        try:
            res = esi.client.Contracts.GetCharactersCharacterIdContracts(
                character_id=character_id,
                token=token,
            )
            esi_contracts = res.results()

            for esi_contract in esi_contracts:
                try:
                    contract = dict(esi_contract)
                except (TypeError, AttributeError):
                    contract = esi_contract.model_dump()

                contract["is_corporation"] = False
                contracts.append(contract)

        except Exception as e:
            # Catch the 304 Not Modified exception safely
            if "HTTPNotModified" in str(type(e)) or "304" in str(e):
                logger.debug(
                    f"Character contracts for {character_id} not modified (304). Skipping."
                )
            else:
                # If it's a real error, log it and re-raise
                logger.error(f"Error fetching character contracts: {e}")
                raise e

        return contracts

    def _fetch_corporation_contracts(self, token) -> list:
        corporation_id = self.character.character.corporation_id
        contracts = []

        try:
            res = esi.client.Contracts.GetCorporationsCorporationIdContracts(
                corporation_id=corporation_id,
                token=token,
            )
            esi_contracts = res.results()

            for esi_contract in esi_contracts:
                try:
                    contract = dict(esi_contract)
                except (TypeError, AttributeError):
                    contract = esi_contract.model_dump()

                contract["is_corporation"] = True
                contracts.append(contract)

        except Exception as e:
            # Catch the 304 Not Modified exception safely
            if "HTTPNotModified" in str(type(e)) or "304" in str(e):
                logger.debug(
                    f"Corporation contracts for {corporation_id} not modified (304). Skipping."
                )
            else:
                logger.error(f"Error fetching corporation contracts: {e}")
                raise e

        return contracts

    def _set_suspicious_contract_notifications(self, contract):
        # List for all notes
        notes = []

        note = ContractNotification(
            contract=contract,
            icon="fa-theater-masks",
            color="red",
            header="Suspicious Contract",
            message="Contract has no tracking object but is has a buyback prefill text! Possibly a scam contract.",
        )

        notes.append(note)

        try:
            ContractNotification.objects.bulk_create(notes)

            logger.debug(
                "Succesfully added items for contract %s into database"
                % contract.contract_id
            )

        except Error as e:
            logger.error(
                "Error adding items for contract %s: %s" % (contract.contract_id, e)
            )

    def _set_watchlist_contract_notifications(self, contract, program):
        # List for all notes
        notes = []

        # Get all watched types and groups for this specific program
        watchlist_types = WatchlistItem.objects.filter(
            program=program, item_type__isnull=False
        ).values_list("item_type_id", flat=True)
        watchlist_groups = WatchlistItem.objects.filter(
            program=program, eve_group__isnull=False
        ).values_list("eve_group_id", flat=True)

        if not watchlist_types and not watchlist_groups:
            # Skip processing if this program doesn't have a watchlist
            return

        flagged_items = set()

        # Check all items in the contract against the watchlist
        for ci in ContractItem.objects.filter(contract=contract).select_related(
            "eve_type"
        ):
            if (
                ci.eve_type_id in watchlist_types
                or ci.eve_type.eve_group_id in watchlist_groups
            ):
                flagged_items.add(ci.eve_type.name)

        # If we found any matches, create the notification
        if flagged_items:
            note = ContractNotification(
                contract=contract,
                icon="fa-search",
                color="purple",
                header="Watchlisted Item",
                message="Manual review recommended. Contains watchlisted items: %s"
                % ", ".join(sorted(list(flagged_items))),
            )
            notes.append(note)

        # Only execute DB call if we actually created a note
        if notes:
            try:
                ContractNotification.objects.bulk_create(notes)
                logger.debug(
                    "Succesfully added watchlist notification for contract %s into database"
                    % contract.contract_id
                )
            except Error as e:
                logger.error(
                    "Error adding watchlist notification for contract %s: %s"
                    % (contract.contract_id, e)
                )

    def _set_contract_notifications(self, tracking, contract, corporations, program):
        # List for all notes/notification icons for contracts
        notes = []

        # Get structure id for tracked contract
        structure_id = Program.objects.filter(pk=program.id).values_list(
            "location__structure_id", flat=True
        )

        logger.debug("Got valid locations for program: %s" % structure_id)

        logger.debug(
            "Checking if items in contract %s match items in tracking %s "
            % (contract.id, tracking.tracking_number)
        )

        # Get items related to tracking object
        tracking_items = list(
            TrackingItem.objects.filter(tracking=tracking)
            .values("eve_type", "quantity")
            .order_by("eve_type", "quantity")
        )

        logger.debug("Got tracking items: %s" % (tracking_items))

        # Get actual contract items
        contract_items = list(
            ContractItem.objects.filter(contract=contract)
            .values("eve_type", "quantity")
            .order_by("eve_type", "quantity")
        )

        logger.debug("Got contract items: %s" % (contract_items))

        if tracking_items != contract_items:
            note = ContractNotification(
                contract=contract,
                icon="fa-unlink",
                color="red",
                header="Item missmatch",
                message="Tracked items do not match the actual items in the contract. See details for more info.",
            )

            notes.append(note)
        else:
            logger.debug("Tracked items match the contract items, all good.")

        # If our tracked price is different than the actual contract price
        # We round the numbers in here as most players do not use decimals ingame and the prices might be off by the digits

        logger.debug(
            "Contract Notes: Tracking price before rounding is set to %s and contract price is set to %s"
            % (tracking.net_price, contract.price)
        )

        tracking_net_price = math.trunc(tracking.net_price)
        contract_net_price = math.trunc(contract.price)

        logger.debug(
            "Contract Notes: Tracking price is set to %s and contract price is set to %s"
            % (tracking_net_price, contract_net_price)
        )

        if tracking_net_price >= 0 and tracking_net_price != contract_net_price:
            # If contract price is bellow tracked price
            if contract_net_price > tracking_net_price:
                note = ContractNotification(
                    contract=contract,
                    icon="fa-dollar-sign",
                    color="red",
                    header="High ask price",
                    message="Ask price is above the calculated price for this contract",
                )

                notes.append(note)

            else:
                note = ContractNotification(
                    contract=contract,
                    icon="fa-dollar-sign",
                    color="orange",
                    header="low ask price",
                    message="Ask price is bellow the calculated price for this contract",
                )

                notes.append(note)

        if structure_id and contract.start_location_id not in structure_id:
            note = ContractNotification(
                contract=contract,
                icon="fa-compass",
                color="red",
                header="Location missmatch",
                message="Contract location does not match program location",
            )

            notes.append(note)

        if contract.assignee_id in corporations and not tracking.program.is_corporation:
            note = ContractNotification(
                contract=contract,
                icon="fa-home",
                color="orange",
                header="Receiver missmatch",
                message="Contract is made for corporation while it should be made directly to the program managers character",
            )

            notes.append(note)

        if contract.assignee_id not in corporations and tracking.program.is_corporation:
            note = ContractNotification(
                contract=contract,
                icon="fa-user",
                color="orange",
                header="Receiver missmatch",
                message="Contract is made for the program managers character while it should be made to the managers corporation",
            )

            notes.append(note)

        if not tracking.tracking_number == contract.title:
            note = ContractNotification(
                contract=contract,
                icon="fa-exclamation",
                color="orange",
                header="Title variation",
                message="Contract description contains extra characterse besides the tracking number. The description should be: '%s', instead it is: '%s'"
                % (tracking.tracking_number, contract.title),
            )

            notes.append(note)

        if tracking.donation:
            note = ContractNotification(
                contract=contract,
                icon="fa-hand-holding-usd",
                color="green",
                header="Donation",
                message="Contact contains a donation",
            )

            notes.append(note)

        if tracking.additional_notes:
            note = ContractNotification(
                contract=contract,
                icon="fa-info-circle",
                color="#17a2b8",
                header="Note from seller",
                message="%s" % tracking.additional_notes,
            )

            notes.append(note)

        if tracking.issuer_user is None:
            note = ContractNotification(
                contract=contract,
                icon="fa-globe",
                color="cornflowerblue",
                header="Public submission",
                message="This contract was submitted by an unauthenticated public user",
            )

            notes.append(note)

        try:
            ContractNotification.objects.bulk_create(notes)

            logger.debug(
                "Succesfully added items for contract %s into database"
                % contract.contract_id
            )

        except Error as e:
            logger.error(
                "Error adding items for contract %s: %s" % (contract.contract_id, e)
            )

    def _set_reverse_contract_notifications(self, tracking, contract):
        """Set notification icons on a reverse buyback contract."""
        notes = []

        # --- Price check ---
        # contract.price stores the 'reward' field (what player pays)
        tracking_price = math.trunc(tracking.net_price)
        contract_price = math.trunc(contract.price)

        if tracking_price >= 0 and tracking_price != contract_price:
            if contract_price < tracking_price:
                note = ContractNotification(
                    contract=contract,
                    icon="fa-dollar-sign",
                    color="orange",
                    header="Low payment",
                    message="Player is paying less than the calculated price for this request.",
                )
            else:
                note = ContractNotification(
                    contract=contract,
                    icon="fa-dollar-sign",
                    color="green",
                    header="High payment",
                    message="Player is paying more than the calculated price for this request.",
                )
            notes.append(note)

        # --- Item check ---
        # ReverseTrackingItem = what player requested
        # ContractItem = what player put in "I will receive"
        tracking_items = list(
            ReverseTrackingItem.objects.filter(tracking=tracking)
            .values("eve_type", "quantity")
            .order_by("eve_type", "quantity")
        )

        contract_items = list(
            ContractItem.objects.filter(contract=contract)
            .values("eve_type", "quantity")
            .order_by("eve_type", "quantity")
        )

        if tracking_items != contract_items:
            note = ContractNotification(
                contract=contract,
                icon="fa-unlink",
                color="red",
                header="Item mismatch",
                message="Items in the contract do not match the requested items from the tracking number.",
            )
            notes.append(note)

        # --- Location check ---
        structure_ids = (
            ReverseProgram.objects.filter(pk=tracking.program.id)
            .values_list("location__structure_id", flat=True)
            .exclude(location__structure_id__isnull=True)
        )

        if structure_ids and contract.start_location_id not in structure_ids:
            note = ContractNotification(
                contract=contract,
                icon="fa-compass",
                color="red",
                header="Location mismatch",
                message="Contract location does not match the reverse program location.",
            )
            notes.append(note)

        # --- Assignee check ---
        corporations = CharacterOwnership.objects.filter(
            user=tracking.program.owner.user
        ).values_list("character__corporation_id", flat=True)

        if contract.assignee_id in corporations and not tracking.program.is_corporation:
            note = ContractNotification(
                contract=contract,
                icon="fa-home",
                color="orange",
                header="Receiver mismatch",
                message="Contract is assigned to the corporation instead of the manager character.",
            )
            notes.append(note)

        if contract.assignee_id not in corporations and tracking.program.is_corporation:
            note = ContractNotification(
                contract=contract,
                icon="fa-user",
                color="orange",
                header="Receiver mismatch",
                message="Contract is assigned to the character instead of the corporation.",
            )
            notes.append(note)

        # --- Title check ---
        if tracking.tracking_number not in contract.title:
            note = ContractNotification(
                contract=contract,
                icon="fa-exclamation",
                color="orange",
                header="Title variation",
                message="Contract title does not match the tracking number. Expected: '%s', got: '%s'"
                % (tracking.tracking_number, contract.title),
            )
            notes.append(note)

        # --- Additional notes ---
        if tracking.additional_notes:
            note = ContractNotification(
                contract=contract,
                icon="fa-info-circle",
                color="#17a2b8",
                header="Note from buyer",
                message="%s" % tracking.additional_notes,
            )
            notes.append(note)

        # --- Public submission ---
        if tracking.issuer_user is None:
            note = ContractNotification(
                contract=contract,
                icon="fa-globe",
                color="cornflowerblue",
                header="Public submission",
                message="This request was submitted by an unauthenticated public user.",
            )
            notes.append(note)

        try:
            ContractNotification.objects.bulk_create(notes)
            logger.debug(
                "Successfully added reverse contract notifications for contract %s"
                % contract.contract_id
            )
        except Error as e:
            logger.error(
                "Error adding reverse contract notifications for contract %s: %s"
                % (contract.contract_id, e)
            )

    def token(self, scopes=None) -> Tuple[Token, int]:
        """returns a valid Token for the owner"""
        token = None
        error = None

        # abort if character is not configured
        if self.character is None:
            logger.error("%s: No character configured to sync", self)
            error = self.ERROR_NO_CHARACTER

        # abort if character does not have sufficient permissions
        elif not (
            self.character.user.has_perm("buybackprogram.manage_programs")
            or self.character.user.has_perm("buybackprogram.manage_all_programs")
        ):
            logger.error(
                "%s: This character does not have sufficient permission to sync contracts",
                self,
            )
            error = self.ERROR_INSUFFICIENT_PERMISSIONS

        else:
            try:
                # get token
                token = (
                    Token.objects.filter(
                        user=self.character.user,
                        character_id=self.character.character.character_id,
                    )
                    .require_scopes(scopes)
                    .require_valid()
                    .first()
                )
            except TokenInvalidError:
                logger.error("%s: Invalid token for fetching calendars", self)
                error = self.ERROR_TOKEN_INVALID
            except TokenExpiredError:
                logger.error("%s: Token expired for fetching calendars", self)
                error = self.ERROR_TOKEN_EXPIRED
            else:
                if not token:
                    logger.error("%s: No token found with sufficient scopes", self)
                    error = self.ERROR_TOKEN_INVALID

        return token, error

    def __str__(self):
        return (
            self.character.character.character_name
            + " ["
            + self.corporation.corporation_ticker
            + "]"
        )


class Location(models.Model):
    """Location where the buyback program is operated at"""

    name = models.CharField(
        max_length=32, help_text="Structure name where the contracts are accepted at"
    )

    eve_solar_system = models.ForeignKey(
        EveSolarSystem,
        verbose_name="Solar system",
        help_text="System where the buyback structure is located",
        blank=True,
        default=None,
        null=True,
        on_delete=models.deletion.SET_DEFAULT,
        related_name="+",
    )

    owner = models.ForeignKey(
        Owner,
        verbose_name="Manager",
        help_text="Player managing this location",
        null=True,
        on_delete=models.deletion.CASCADE,
    )

    structure_id = models.BigIntegerField(
        verbose_name="Ingame unique ID for structure",
        default=None,
        blank=True,
        null=True,
        help_text="The ID for the structure you wish to accept the contracts at. If left empty the program statistics page will not track if the contract is actually made at the correct structure or not. To get the ID for the structure see readme for getting structure IDs",
    )

    asset_location_id = models.BigIntegerField(
        verbose_name="Asset system location ID",
        default=None,
        blank=True,
        null=True,
        help_text="The ESI asset system location ID for this structure. Differs from Structure ID on some structures. Leave blank to use Structure ID.",
    )

    def __str__(self):
        system_name = self.eve_solar_system.name if self.eve_solar_system else "CUSTOM"
        return system_name + " | " + self.name + ", ID: " + str(self.structure_id)

    @property
    def location_display_name(self):
        # Check if eve_solar_system exists before accessing .name
        if self.eve_solar_system:
            return f"{self.eve_solar_system.name}: {self.name}"
        else:
            return f"{self.name}"


class WalletDivision(models.Model):
    """Cache for Wallet Division names and balances."""

    owner = models.ForeignKey(Owner, on_delete=models.CASCADE, related_name="wallets")
    division_id = models.IntegerField()
    name = models.CharField(max_length=128)
    balance = models.DecimalField(max_digits=20, decimal_places=2, default=0.0)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        unique_together = (("owner", "division_id"),)
        default_permissions = ()

    def __str__(self):
        # Displays like "Ore Buyback Wallet (1,234,567.89 ISK)"
        return f"{self.name} ({self.balance:,.2f} ISK)"


class Program(models.Model):
    """An Eve Online buyback program"""

    class Expiration(models.TextChoices):
        DAY1 = "1 Day", _("1 Day")
        DAY3 = "3 Days", _("3 Days")
        WEEK1 = "1 Week", _("1 Week")
        WEEK2 = "2 Weeks", _("2 Weeks")
        WEEK4 = "4 Weeks", _("4 Weeks")

    class PriceType(models.TextChoices):
        BUY = "Buy", _("Buy")
        SELL = "Sell", _("Sell")
        SPLIT = "Split", _("Split")

    name = models.CharField(
        verbose_name="Name/description",
        max_length=64,
        help_text="A name or a description for this program",
        blank=True,
        default="",
    )

    owner = models.ForeignKey(
        Owner,
        verbose_name="Manager",
        help_text="Character that is used to manage this program.",
        on_delete=models.deletion.CASCADE,
        related_name="+",
    )

    is_corporation = models.BooleanField(
        default=False,
        help_text="If we should use the corporation of the manager as the contract receiver instead of the character.",
    )

    location = models.ManyToManyField(
        Location,
        help_text="The location where contracts should be created at.",
        related_name="+",
    )

    expiration = models.CharField(
        max_length=7,
        choices=Expiration.choices,
        default=Expiration.WEEK2,
        help_text="Expiration time the contracts should bet set to.",
    )

    price_type = models.CharField(
        max_length=7,
        choices=PriceType.choices,
        default=PriceType.BUY,
        help_text="What prices should we use as the source for prices. Default: Buy",
    )

    tax = models.IntegerField(
        verbose_name="Default tax",
        default=0,
        blank=False,
        null=False,
        help_text="A default tax rate in this program that is applied on all items.",
        validators=[MaxValueValidator(100), MinValueValidator(0)],
    )

    hauling_fuel_cost = models.IntegerField(
        verbose_name="Hauling fuel cost per m³",
        default=0,
        help_text="ISK per m³ that will be removed from the buy price ie. to cover jump freighet fuel costs. <b>Should not be used with price dencity modifier</b>",
    )

    price_dencity_modifier = models.BooleanField(
        verbose_name="Price density modifier",
        default=False,
        help_text="Should we modify buy prices for items with high volume and low value ie. T1 industrial hulls. <b>Should not be used with hauling fuel cost</b>",
    )

    compression_price_dencity_modifier = models.BooleanField(
        verbose_name="Ore volume based on compressed volume",
        default=False,
        help_text="Should we use compressed volume for items that can be compressed such as ore and ice in fuel cost and price dencity calculations. If set to False the raw volume is used for volume calculations if the item does not have a compressed price variant available from the use compressed value setting.",
    )

    price_dencity_treshold = models.IntegerField(
        verbose_name="Price density threshold",
        default=0,
        null=True,
        help_text="At what ISK/m3 do we start to apply the low isk dencity tax. Tritanium is 500 ISK/m³ @ 5 ISK per unit price. PLEX is 14,5Trillion ISK/m³ @2.9M per unit price.",
    )

    price_dencity_tax = models.IntegerField(
        verbose_name="Price density tax",
        default=0,
        null=True,
        help_text="How much tax do we apply on the low isk density items.",
        validators=[MaxValueValidator(100), MinValueValidator(0)],
    )

    allow_all_items = models.BooleanField(
        default=True,
        help_text="If true all items are accepted to the buyback program. You can set extra taxes or disallow individual items from the program item section. If set to false you need to add each accepted item into the program item section. Blueprints are not included in all items.",
    )

    use_refined_value = models.BooleanField(
        verbose_name="Ore: Use refined value",
        default=False,
        help_text="Take refined value into account when calculating prices for ore, ice and moon goo",
    )

    use_compressed_value = models.BooleanField(
        verbose_name="Ore: Use compressed value",
        default=False,
        help_text="Take compressed value into account when calculating prices for ore, ice and moon goo",
    )

    use_raw_ore_value = models.BooleanField(
        verbose_name="Ore: Use raw value",
        default=True,
        help_text="Take raw ore value into account when calculating prices for ore, ice and moon goo",
    )

    allow_unpacked_items = models.BooleanField(
        verbose_name="Allow unpacked items",
        default=False,
        help_text="Do you want to allow unpacked items in this program such as assembled ship hulls?",
    )

    refining_rate = models.DecimalField(
        verbose_name="Refining rate",
        max_digits=5,
        decimal_places=2,
        default=0,
        null=True,
        help_text="Refining rate to be used if ore refined value is active",
        validators=[MaxValueValidator(100), MinValueValidator(0)],
    )

    use_reprocessed_value_for_t1 = models.BooleanField(
        verbose_name="Use reprocessed value for T1/Named (Meta 1-4)",
        default=False,
        help_text="If enabled, ships, modules, and drones with a meta level of 4 or lower will be valued based on their reprocessed mineral value using the program's refining rate.",
    )

    t1_refining_rate = models.DecimalField(
        max_digits=5,
        decimal_places=2,
        default=50.00,
        verbose_name="T1 Scrap Refining Rate (%)",
        help_text="The reprocessing efficiency used exclusively for calculating the scrap value of modules, ships, and drones.",
    )

    blue_loot_npc_price = models.BooleanField(
        verbose_name="NPC price for: Sleeper Components",
        default=False,
        help_text="Use NPC price as value for blue loot",
    )

    red_loot_npc_price = models.BooleanField(
        verbose_name="NPC price for: Triglavian Survey Database",
        default=False,
        help_text="Use NPC price as value for red loot",
    )

    ope_npc_price = models.BooleanField(
        verbose_name="NPC price for: Overseer's Personal Effects",
        default=False,
        help_text="Use NPC price as value for OPEs",
    )

    bonds_npc_price = models.BooleanField(
        verbose_name="NPC price for: Bounty Encrypted Bonds",
        default=False,
        help_text="Use NPC price as value for SCC Encrypted Bond",
    )

    restricted_to_group = models.ManyToManyField(
        Group,
        blank=True,
        related_name="buybackprogram_require_groups",
        help_text="The group(s) that will be able to see this buyback program. If none is selected program is open for all.",
    )
    restricted_to_state = models.ManyToManyField(
        State,
        blank=True,
        related_name="buybackprogram_require_states",
        help_text="The state(s) that will be able to see this buyback program. If none is selected program is open for all.",
    )

    is_public = models.BooleanField(
        verbose_name="Public calculator",
        default=False,
        help_text="If enabled, unauthenticated users can access the price calculator for this program via a public URL.",
    )

    discord_dm_notification = models.BooleanField(
        verbose_name="Discord direct messages for new contracts",
        default=False,
        help_text="Check if you want to receive a direct message notification each time a new contract is created. <b>Requires aa-discordbot app or discordproxy app to work</b>",
    )

    discord_show_item_list = models.BooleanField(
        verbose_name="Show list of items on discord message",
        default=False,
        help_text="Determines if you want to show the contract items in the discord messages. This applies to both webhooks and direct messages.",
    )

    discord_channel_notification = models.CharField(
        verbose_name="Discord webhook for notifications",
        max_length=256,
        null=True,
        blank=True,
        help_text="Discord webhook to send contract notifications to.",
    )

    wallet = models.ForeignKey(
        WalletDivision,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        help_text="The wallet division used to fund this buyback program.",
    )

    class Meta:
        default_permissions = ()

    def user_can_manage(self, user):
        """Returns True if user is the owner OR has global admin permission"""
        # Global permission bypasses ownership check entirely
        if user.has_perm("buybackprogram.manage_all_programs"):
            return True

        # Otherwise, strictly check ownership
        return self.owner.user == user

    def clean(self):
        super().clean()
        if (
            self.allow_all_items
            and not self.use_refined_value
            and not self.use_compressed_value
            and not self.use_raw_ore_value
        ):
            raise ValidationError(
                "All items are allowed but not a single pricing method for ores is selected. Please use at least one pricing method for ores if all items is allowed."
            )
        if self.price_dencity_modifier and not self.price_dencity_tax:
            raise ValidationError(
                "Price density is used but value for price density tax is missing"
            )
        if self.price_dencity_modifier and not self.price_dencity_treshold:
            raise ValidationError(
                "Price density is used but value for price density threshold is missing"
            )
        if self.use_refined_value and not self.refining_rate:
            raise ValidationError(
                "Refined value is used for ore pricing method but no refining rate is provided. Provide a refining rate to used with this pricing model."
            )


class ProgramItem(models.Model):
    """Items in the buyback program for a corp"""

    program = models.ForeignKey(
        Program,
        on_delete=models.deletion.CASCADE,
        help_text="What program do these items belong to",
    )
    item_type = models.ForeignKey(
        EveType,
        on_delete=models.deletion.CASCADE,
        null=True,
        blank=True,
        help_text="Select a specific item for special tax",
    )
    market_group = models.ForeignKey(
        EveMarketGroup,
        on_delete=models.deletion.CASCADE,
        null=True,
        blank=True,
        help_text="Select a market group for special tax",
    )
    item_tax = models.IntegerField(
        verbose_name="Item tax adjustment",
        default=0,
        null=True,
        help_text="How much do you want to adjust the default tax on this item.",
        validators=[MaxValueValidator(100), MinValueValidator(-100)],
    )
    disallow_item = models.BooleanField(
        verbose_name="Disallow item in program",
        default=False,
        help_text="Disallow this item or market group from the program.",
    )

    class Meta:
        default_permissions = ()
        unique_together = ["program", "item_type"]


class WatchlistItem(models.Model):
    """Items or groups that require manual review when traded."""

    program = models.ForeignKey(
        Program,
        on_delete=models.CASCADE,
        help_text="The program this watchlist applies to",
    )
    item_type = models.ForeignKey(
        EveType,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="Specific item to watch (leave blank if watching a whole group)",
    )
    eve_group = models.ForeignKey(
        EveGroup,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="Entire item group to watch (e.g. 'Officer Modules')",
    )

    class Meta:
        default_permissions = ()
        unique_together = ["program", "item_type", "eve_group"]

    def clean(self):
        super().clean()
        if not self.item_type and not self.eve_group:
            raise ValidationError("You must select either an item or a group to watch.")
        if self.item_type and self.eve_group:
            raise ValidationError("Select either an item or a group, not both.")

    def __str__(self):
        if self.item_type:
            return f"Item: {self.item_type.name}"
        return f"Group: {self.eve_group.name}"


class ItemPrices(models.Model):
    eve_type = models.OneToOneField(
        EveType,
        on_delete=models.deletion.CASCADE,
        unique=True,
    )
    buy = models.DecimalField(max_digits=20, decimal_places=2)
    sell = models.DecimalField(max_digits=20, decimal_places=2)
    updated = models.DateTimeField()


class Contract(models.Model):
    assignee_id = models.IntegerField()
    availability = models.CharField(max_length=20)
    contract_id = models.IntegerField(unique=True)
    date_completed = models.DateTimeField(null=True)
    date_expired = models.DateTimeField(null=True)
    date_issued = models.DateTimeField()
    for_corporation = models.BooleanField()
    issuer_corporation_id = models.IntegerField()
    issuer_id = models.IntegerField()
    start_location_id = models.BigIntegerField(null=True)
    location_name = models.CharField(max_length=128, null=True)
    price = models.DecimalField(max_digits=20, decimal_places=2)
    status = models.CharField(max_length=30)
    title = models.CharField(max_length=128)
    volume = models.DecimalField(max_digits=20, decimal_places=2)
    no_tracking = models.BooleanField(default=False)

    def __str__(self) -> str:
        return str(self.contract_id)


class ContractItem(models.Model):
    contract = models.ForeignKey(
        Contract,
        on_delete=models.deletion.CASCADE,
        help_text="What contract do these items belong to",
    )

    eve_type = models.ForeignKey(
        EveType,
        on_delete=models.deletion.CASCADE,
        help_text="Item type information",
    )

    quantity = models.IntegerField()


class ContractNotification(models.Model):
    contract = models.ForeignKey(
        Contract,
        on_delete=models.deletion.CASCADE,
    )

    icon = models.CharField(
        max_length=64,
    )

    color = models.CharField(
        max_length=32,
    )

    header = models.CharField(
        max_length=1024,
        null=True,
        blank=True,
    )

    message = models.CharField(
        max_length=1024,
    )


class Tracking(models.Model):
    program = models.ForeignKey(
        Program,
        null=True,
        on_delete=models.deletion.SET_NULL,
        related_name="+",
    )
    contract = models.ForeignKey(
        Contract,
        null=True,
        blank=True,
        on_delete=models.deletion.SET_NULL,
    )
    issuer_user = models.ForeignKey(
        User,
        on_delete=models.deletion.CASCADE,
        null=True,
        blank=True,
        related_name="+",
    )
    value = models.DecimalField(max_digits=20, decimal_places=2, null=False)
    taxes = models.DecimalField(max_digits=20, decimal_places=2, null=False)
    hauling_cost = models.DecimalField(max_digits=20, decimal_places=2, null=False)
    donation = models.DecimalField(
        max_digits=20, decimal_places=2, null=True, blank=True
    )
    net_price = models.DecimalField(max_digits=20, decimal_places=2, null=False)
    tracking_number = models.CharField(max_length=32)
    created_at = models.DateTimeField(null=True, blank=True)
    additional_notes = models.TextField(null=True, blank=True)


class TrackingItem(models.Model):
    tracking = models.ForeignKey(
        Tracking,
        on_delete=models.deletion.CASCADE,
        help_text="What tracking do these items belong to",
    )

    eve_type = models.ForeignKey(
        EveType,
        on_delete=models.deletion.CASCADE,
        help_text="Item type information",
    )

    buy_value = models.DecimalField(max_digits=20, decimal_places=2, null=False)

    quantity = models.IntegerField()


class UserSettings(models.Model):
    """
    User settings
    """

    user = models.ForeignKey(
        User,
        related_name="+",
        null=True,
        blank=True,
        default=None,
        on_delete=models.SET(get_sentinel_user),
    )

    disable_notifications = models.BooleanField(
        default=False,
    )

    class Meta:
        """
        Meta definitions
        """

        default_permissions = ()
        verbose_name = _("User Settings")
        verbose_name_plural = _("User Settings")


class Faq(models.Model):
    header = models.CharField(
        max_length=1024,
    )

    body = models.TextField()


class StructureNameCache(models.Model):
    """Local cache for structure names to avoid ESI lookups and 304 errors"""

    structure_id = models.BigIntegerField(primary_key=True)
    name = models.CharField(max_length=255)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return f"{self.structure_id}: {self.name}"


class StaticItemPrice(models.Model):
    """Allows setting a manual/static buy price for items in a specific program"""

    program = models.ForeignKey(
        Program,
        on_delete=models.CASCADE,
        related_name="static_prices",
        help_text="The program this price applies to",
    )
    eve_type = models.ForeignKey(
        EveType,
        on_delete=models.CASCADE,
        help_text="The item to set a static price for",
    )
    price = models.DecimalField(
        max_digits=20,
        decimal_places=2,
        help_text="The static price to use instead of market data",
    )

    class Meta:
        unique_together = ["program", "eve_type"]
        default_permissions = ()


class HangarDivision(models.Model):
    LOCATION_FLAGS = {
        1: "CorpSAG1",
        2: "CorpSAG2",
        3: "CorpSAG3",
        4: "CorpSAG4",
        5: "CorpSAG5",
        6: "CorpSAG6",
    }
    owner = models.ForeignKey(Owner, on_delete=models.CASCADE, related_name="hangars")
    division_id = models.IntegerField()
    name = models.CharField(max_length=128)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        unique_together = (("owner", "division_id"),)
        default_permissions = ()

    @property
    def location_flag(self):
        return self.LOCATION_FLAGS.get(self.division_id, "Hangar")

    def __str__(self):
        return self.name


class ReverseProgram(models.Model):
    """A reverse buyback program where the owner sells items to players"""

    name = models.CharField(
        max_length=64,
        help_text="A name or description for this reverse buyback program.",
    )

    owner = models.ForeignKey(
        Owner,
        verbose_name="Manager",
        on_delete=models.deletion.CASCADE,
        related_name="+",
    )

    is_corporation = models.BooleanField(
        default=False,
        help_text="If the contract should be issued from the owner's corporation.",
    )

    location = models.ManyToManyField(
        Location,
        help_text="Locations where stock is held and contracts will be created.",
        related_name="+",
    )

    hangar_division = models.ForeignKey(
        HangarDivision,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        help_text="The corporation hangar division where stock is stored.",
    )

    expiration = models.CharField(
        max_length=7,
        choices=Program.Expiration.choices,
        default=Program.Expiration.WEEK2,
        help_text="Expiration time for contracts.",
    )

    price_type = models.CharField(
        max_length=7,
        choices=Program.PriceType.choices,
        default=Program.PriceType.SELL,
        help_text="Price type to use as the base price. Default: Sell",
    )

    markup = models.IntegerField(
        verbose_name="Sell markup %",
        default=0,
        validators=[MaxValueValidator(100), MinValueValidator(-100)],
        help_text="Markup percentage added on top of the base price. May also be negative.",
    )

    restricted_to_group = models.ManyToManyField(
        Group,
        blank=True,
        related_name="buybackprogram_reverse_require_groups",
        help_text="The group(s) that will be able to see this reverse buyback program. If none is selected program is open for all.",
    )
    restricted_to_state = models.ManyToManyField(
        State,
        blank=True,
        related_name="buybackprogram_reverse_require_states",
        help_text="The state(s) that will be able to see this reverse buyback program. If none is selected program is open for all.",
    )

    is_public = models.BooleanField(
        verbose_name="Public program",
        default=False,
        help_text="If enabled, this program is visible and usable without login.",
    )
    discord_dm_notification = models.BooleanField(
        verbose_name="Discord direct messages for new requests",
        default=False,
        help_text="Receive a DM notification each time a new buy request is created.",
    )

    discord_channel_notification = models.CharField(
        verbose_name="Discord webhook for notifications",
        max_length=256,
        null=True,
        blank=True,
        help_text="Discord webhook to send buy request notifications to.",
    )

    class Meta:
        default_permissions = ()

    def user_can_manage(self, user):
        if user.has_perm("buybackprogram.manage_all_programs"):
            return True
        return self.owner.user == user


class HangarStock(models.Model):
    """Current inventory available in a reverse buyback program"""

    program = models.ForeignKey(
        ReverseProgram,
        on_delete=models.deletion.CASCADE,
        related_name="stock",
    )

    eve_type = models.ForeignKey(
        EveType,
        on_delete=models.deletion.CASCADE,
    )

    quantity = models.IntegerField()

    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        unique_together = ("program", "eve_type")
        default_permissions = ()


class ReverseTracking(models.Model):
    """Tracks a player's buy request against a reverse buyback program"""

    program = models.ForeignKey(
        ReverseProgram,
        null=True,
        on_delete=models.deletion.SET_NULL,
        related_name="+",
    )

    contract = models.ForeignKey(
        Contract,
        null=True,
        blank=True,
        on_delete=models.deletion.SET_NULL,
    )

    issuer_user = models.ForeignKey(
        User,
        null=True,
        blank=True,
        on_delete=models.deletion.SET_NULL,
        related_name="+",
    )

    net_price = models.DecimalField(max_digits=20, decimal_places=2)
    tracking_number = models.CharField(max_length=32)
    created_at = models.DateTimeField(null=True, blank=True)
    additional_notes = models.TextField(null=True, blank=True)

    class Meta:
        default_permissions = ()


class ReverseTrackingItem(models.Model):
    """Items included in a reverse buyback tracking record"""

    tracking = models.ForeignKey(
        ReverseTracking,
        on_delete=models.deletion.CASCADE,
    )

    eve_type = models.ForeignKey(
        EveType,
        on_delete=models.deletion.CASCADE,
    )

    quantity = models.IntegerField()

    buy_value = models.DecimalField(max_digits=20, decimal_places=2)

    class Meta:
        default_permissions = ()
