Files
Example-TCG-Site/store/models.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

272 lines
12 KiB
Python

from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
from django.conf import settings
import uuid
class Game(models.Model):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class Set(models.Model):
game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name='sets')
name = models.CharField(max_length=200)
code = models.CharField(max_length=20, blank=True)
release_date = models.DateField(null=True, blank=True)
def __str__(self):
return f"{self.game.name} - {self.name}"
class Seller(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='seller_profile')
store_name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
description = models.TextField(blank=True)
store_image = models.ImageField(upload_to='seller_images/', blank=True, null=True)
hero_image = models.ImageField(upload_to='seller_hero_images/', blank=True, null=True)
contact_email = models.EmailField(blank=True)
contact_phone = models.CharField(max_length=20, blank=True)
business_address = models.TextField(blank=True)
store_views = models.PositiveIntegerField(default=0)
listing_clicks = models.PositiveIntegerField(default=0)
minimum_order_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
shipping_cost = models.DecimalField(max_digits=10, decimal_places=2, default=0)
created_at = models.DateTimeField(auto_now_add=True)
tax_id_encrypted = models.BinaryField(blank=True, null=True)
payout_details_encrypted = models.BinaryField(blank=True, null=True)
@property
def tax_id(self):
from .utils import Encryptor
return Encryptor.decrypt(self.tax_id_encrypted)
@tax_id.setter
def tax_id(self, value):
from .utils import Encryptor
self.tax_id_encrypted = Encryptor.encrypt(value)
@property
def payout_details(self):
from .utils import Encryptor
return Encryptor.decrypt(self.payout_details_encrypted)
@payout_details.setter
def payout_details(self, value):
from .utils import Encryptor
self.payout_details_encrypted = Encryptor.encrypt(value)
def __str__(self):
return self.store_name
class Card(models.Model):
set = models.ForeignKey(Set, on_delete=models.CASCADE, related_name='cards')
name = models.CharField(max_length=200)
rarity = models.CharField(max_length=50, blank=True)
image_url = models.URLField(max_length=500, blank=True)
scryfall_id = models.CharField(max_length=100, blank=True, null=True)
tcgplayer_id = models.CharField(max_length=100, blank=True, null=True)
collector_number = models.CharField(max_length=50, blank=True)
external_url = models.URLField(max_length=500, blank=True, help_text="Link to official card page (e.g. Scryfall, Lorcast)")
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
def __str__(self):
return self.name
class CardListing(models.Model):
CONDITION_CHOICES = (
('NM', 'Near Mint'),
('LP', 'Lightly Played'),
('MP', 'Moderately Played'),
('HP', 'Heavily Played'),
)
STATUS_CHOICES = (
('listed', 'Listed'),
('sold', 'Sold'),
('off_market', 'Off Market'),
)
card = models.ForeignKey(Card, on_delete=models.CASCADE, related_name='listings')
seller = models.ForeignKey(Seller, on_delete=models.CASCADE, related_name='card_listings', null=True, blank=True)
condition = models.CharField(max_length=2, choices=CONDITION_CHOICES, default='NM')
price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.PositiveIntegerField(default=0)
market_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
is_foil = models.BooleanField(default=False)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='listed')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='listed')
image = models.ImageField(upload_to='listing_images/', blank=True, null=True)
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
def __str__(self):
return f"{self.card.name} ({self.condition}) - ${self.price} [{self.status}]"
class PackListing(models.Model):
LISTING_TYPE_CHOICES = (
('physical', 'Physical (Shipped)'),
('virtual', 'Virtual (Open on Store)'),
)
game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name='pack_listings')
seller = models.ForeignKey(Seller, on_delete=models.CASCADE, related_name='pack_listings', null=True, blank=True)
name = models.CharField(max_length=200)
listing_type = models.CharField(max_length=10, choices=LISTING_TYPE_CHOICES, default='physical')
price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.PositiveIntegerField(default=0)
quantity = models.PositiveIntegerField(default=0)
image_url = models.URLField(max_length=500, blank=True)
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
def __str__(self):
return f"{self.name} - ${self.price}"
class VirtualPack(models.Model):
STATUS_CHOICES = (
('sealed', 'Sealed'),
('opened', 'Opened'),
)
listing = models.ForeignKey(PackListing, on_delete=models.CASCADE, related_name='packs')
cards = models.ManyToManyField(Card, related_name='packs')
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='sealed')
owner = models.ForeignKey('users.Buyer', on_delete=models.SET_NULL, null=True, blank=True, related_name='packs')
created_at = models.DateTimeField(auto_now_add=True)
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
def __str__(self):
return f"{self.listing.name} ({self.get_status_display()})"
class Order(models.Model):
STATUS_CHOICES = (
('pending', 'Pending'),
('paid', 'Paid'),
('shipped', 'Shipped'),
('cancelled', 'Cancelled'),
)
buyer = models.ForeignKey('users.Buyer', on_delete=models.CASCADE, related_name='orders')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
total_price = models.DecimalField(max_digits=12, decimal_places=2, default=0)
stripe_payment_intent = models.CharField(max_length=100, blank=True)
shipping_address = models.TextField(blank=True)
insurance_purchased = models.BooleanField(default=False)
proxy_service = models.BooleanField(default=False)
seller = models.ForeignKey(Seller, on_delete=models.SET_NULL, related_name='orders', null=True, blank=True)
rating = models.PositiveSmallIntegerField(null=True, blank=True, validators=[MinValueValidator(1), MaxValueValidator(5)])
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
def __str__(self):
return f"Order #{self.id} - {self.buyer.user.username}"
class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items')
listing = models.ForeignKey(CardListing, on_delete=models.SET_NULL, null=True, blank=True)
pack_listing = models.ForeignKey(PackListing, on_delete=models.SET_NULL, null=True, blank=True)
price_at_purchase = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.PositiveIntegerField(default=1)
def __str__(self):
if self.pack_listing:
return f"{self.quantity}x {self.pack_listing.name}"
return f"{self.quantity}x {self.listing.card.name if self.listing else 'Deleted Listing'}"
class Cart(models.Model):
buyer = models.OneToOneField('users.Buyer', on_delete=models.CASCADE, related_name='cart', null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
insurance = models.BooleanField(default=False)
@property
def total_price(self):
base_total = sum(item.total_price for item in self.items.all())
if self.insurance:
return base_total + 5 # Flat fee
return base_total
class CartItem(models.Model):
cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name='items')
listing = models.ForeignKey(CardListing, on_delete=models.CASCADE, null=True, blank=True)
pack_listing = models.ForeignKey(PackListing, on_delete=models.CASCADE, null=True, blank=True)
quantity = models.PositiveIntegerField(default=1)
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
@property
def total_price(self):
if self.listing:
return self.listing.price * self.quantity
if self.pack_listing:
return self.pack_listing.price * self.quantity
return 0
class Bounty(models.Model):
seller = models.ForeignKey(Seller, on_delete=models.CASCADE, related_name='bounties', null=True)
card = models.ForeignKey(Card, on_delete=models.SET_NULL, related_name='bounties', null=True, blank=True)
title = models.CharField(max_length=200, blank=True, help_text="Required if no card selected")
description = models.TextField(blank=True)
target_price = models.DecimalField(max_digits=10, decimal_places=2)
quantity_wanted = models.PositiveIntegerField(default=1)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
def __str__(self):
if self.card:
return f"WANTED: {self.card.name} @ ${self.target_price}"
return f"WANTED: {self.title} @ ${self.target_price}"
def save(self, *args, **kwargs):
if not self.card and not self.title:
raise ValueError("Bounty must have either a Card or a Title")
if self.card and not self.title:
self.title = f"Buying {self.card.name}"
super().save(*args, **kwargs)
class BountyOffer(models.Model):
STATUS_CHOICES = (
('pending', 'Pending'),
('accepted', 'Accepted'),
('rejected', 'Rejected'),
('countered', 'Countered'),
)
bounty = models.ForeignKey(Bounty, on_delete=models.CASCADE, related_name='offers')
buyer = models.ForeignKey('users.Buyer', on_delete=models.CASCADE, related_name='bounty_offers')
price = models.DecimalField(max_digits=10, decimal_places=2)
description = models.TextField(blank=True, help_text="Details about what you are offering (e.g. card condition)")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
created_at = models.DateTimeField(auto_now_add=True)
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
def __str__(self):
return f"Offer of ${self.price} by {self.buyer.user.username} on {self.bounty}"
class VaultItem(models.Model):
buyer = models.ForeignKey('users.Buyer', on_delete=models.CASCADE, related_name='vault_items')
card = models.ForeignKey(Card, on_delete=models.CASCADE, related_name='vault_items')
quantity = models.PositiveIntegerField(default=1)
added_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('buyer', 'card')
def __str__(self):
return f"{self.buyer.user.username}'s {self.card.name} ({self.quantity})"
class SellerReport(models.Model):
reporter = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='seller_reports')
seller = models.ForeignKey(Seller, on_delete=models.CASCADE, related_name='reports')
REASON_CHOICES = [
('explicit', 'Explicit/NSFW Content'),
('scam', 'Scam'),
('other', 'Other'),
]
reason = models.CharField(max_length=20, choices=REASON_CHOICES)
details = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Report by {self.reporter} on {self.seller.store_name} - {self.get_reason_display()}"