import re from .models import Card, CardListing, Order, OrderItem, VaultItem from django.db.models import Min import base64 from cryptography.fernet import Fernet from django.conf import settings from decimal import Decimal def add_to_vault(buyer, card, quantity=1): """ Adds a card to the buyer's vault. """ vault_item, created = VaultItem.objects.get_or_create(buyer=buyer, card=card) if not created: vault_item.quantity += quantity else: vault_item.quantity = quantity vault_item.save() def parse_deck_list(deck_text): """ Parses a deck list string and returns a list of dictionaries with 'quantity' and 'name'. Format expected: "4 Lightning Bolt" or "1x Lightning Bolt" """ lines = deck_text.strip().split('\n') parsed_cards = [] for line in lines: line = line.strip() if not line: continue match = re.match(r'^(\d+)[xX]?\s+(.+)$', line) if match: quantity = int(match.group(1)) name = match.group(2).strip() parsed_cards.append({'quantity': quantity, 'name': name}) else: # Maybe just name? assume 1 parsed_cards.append({'quantity': 1, 'name': line}) return parsed_cards def find_best_listings_for_deck(parsed_cards): """ Finds cheapest listings for the parsed cards. Returns: - found_items: list of {listing, quantity_needed, total_cost, card_name} - missing_items: list of {name, quantity} """ found_items = [] missing_items = [] for item in parsed_cards: name = item['name'] qty_needed = item['quantity'] # Find card (simple name match) cards = Card.objects.filter(name__iexact=name) if not cards.exists(): # Try contains cards = Card.objects.filter(name__icontains=name) if not cards.exists(): missing_items.append(item) continue # Find cheapest listing with stock # We try to fill the quantity from multiple listings if needed listings = CardListing.objects.filter( card__in=cards, quantity__gt=0 ).order_by('price') qty_remaining = qty_needed for listing in listings: if qty_remaining <= 0: break qty_to_take = min(listing.quantity, qty_remaining) found_items.append({ 'listing': listing, 'quantity': qty_to_take, 'card_name': listing.card.name, 'price': listing.price, 'total': listing.price * qty_to_take }) qty_remaining -= qty_to_take if qty_remaining > 0: missing_items.append({'name': name, 'quantity': qty_remaining}) return found_items, missing_items def get_user_collection(user): """ Returns a dict {card_name: quantity} of cards in user's vault. """ owned = {} if not user.is_authenticated or not hasattr(user, 'buyer_profile'): return owned vault_items = VaultItem.objects.filter(buyer=user.buyer_profile).select_related('card') for item in vault_items: owned[item.card.name] = item.quantity return owned def filter_deck_by_collection(parsed_cards, owned_cards): """ Subtracts owned quantities from parsed_cards. Returns new list of parsed_cards. """ filtered = [] for item in parsed_cards: name = item['name'] needed = item['quantity'] # Simple name match owned_qty = 0 # Try exact match first if name in owned_cards: owned_qty = owned_cards[name] else: # Try case insensitive fallback for key in owned_cards: if key.lower() == name.lower(): owned_qty = owned_cards[key] break remaining = needed - owned_qty if remaining > 0: filtered.append({'name': name, 'quantity': remaining}) return filtered class Encryptor: """ Utility for encrypting and decrypting sensitive data using Fernet. Derives a key from settings.SECRET_KEY. """ _cipher = None @classmethod def get_cipher(cls): if cls._cipher is None: # Derive a 32-byte key from SECRET_KEY # Ensure key is url-safe base64-encoded 32-byte key # We use hashlib to ensure we get a valid 32-byte key for Fernet, # regardless of SECRET_KEY length. import hashlib key_hash = hashlib.sha256(settings.SECRET_KEY.encode('utf-8')).digest() key_b64 = base64.urlsafe_b64encode(key_hash) cls._cipher = Fernet(key_b64) return cls._cipher @classmethod def encrypt(cls, plaintext): if not plaintext: return None if isinstance(plaintext, str): plaintext = plaintext.encode('utf-8') return cls.get_cipher().encrypt(plaintext) @classmethod def decrypt(cls, ciphertext): if not ciphertext: return None if isinstance(ciphertext, memoryview): ciphertext = bytes(ciphertext) try: return cls.get_cipher().decrypt(ciphertext).decode('utf-8') except Exception: return None def calculate_platform_fee(total_amount): """ Calculates platform fee: 5% + $0.70, capped at $25. """ if not total_amount: return Decimal('0.00') fee = (total_amount * Decimal('0.05')) + Decimal('0.70') return min(fee, Decimal('25.00'))