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
188 lines
5.7 KiB
Python
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'))
|