Files
Example-TCG-Site/store/utils.py
Ryan Westfall 9040021d1b MASSIVE UPDATE:
bounty board feature

buyers to see bounty boards

seller profile page (like have theme chooser)

Have the game and set name be filters.

Add cards to vault manually

update card inventory add to have the autocomplete for the card  -

store analytics, clicks, views, link to store (url/QR code)

bulk item inventory creation --

Make the banner feature flag driven so I can have a beta site setup like the primary site

don't use primary key values in urls - update to use uuid4 values

site analytics. tianji is being sent

item potent on the mtg and lorcana populate scripts

Card item images for specific listings

check that when you buy a card it is in the vault

Buys should be able to search on store inventories

More pie charts for the seller!

post bounty board is slow to load

seller reviews/ratings - show a historgram - need a way for someone to rate

Report a seller feature for buyer to report

Make sure the stlying is consistent based on the theme choosen

smart minimum order quantity and shipping amounts (defined by the store itself)

put virtual packs behind a feature flag like bounty board

proxy service feature flag

Terms of Service

new description for TCGKof

store SSN, ITIN, and EIN

optomize for SEO
2026-01-23 12:28:20 -06:00

188 lines
5.7 KiB
Python

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'))