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
This commit is contained in:
139
store/models.py
139
store/models.py
@@ -1,5 +1,7 @@
|
||||
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)
|
||||
@@ -17,6 +19,48 @@ class Set(models.Model):
|
||||
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)
|
||||
@@ -25,10 +69,13 @@ class Card(models.Model):
|
||||
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'),
|
||||
@@ -37,21 +84,42 @@ class CardListing(models.Model):
|
||||
('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}"
|
||||
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}"
|
||||
@@ -64,8 +132,9 @@ class VirtualPack(models.Model):
|
||||
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(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='packs')
|
||||
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()})"
|
||||
@@ -78,7 +147,7 @@ class Order(models.Model):
|
||||
('cancelled', 'Cancelled'),
|
||||
)
|
||||
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='orders')
|
||||
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)
|
||||
@@ -87,9 +156,12 @@ class Order(models.Model):
|
||||
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.user.username}"
|
||||
return f"Order #{self.id} - {self.buyer.user.username}"
|
||||
|
||||
class OrderItem(models.Model):
|
||||
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items')
|
||||
@@ -104,7 +176,7 @@ class OrderItem(models.Model):
|
||||
return f"{self.quantity}x {self.listing.card.name if self.listing else 'Deleted Listing'}"
|
||||
|
||||
class Cart(models.Model):
|
||||
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='cart', null=True, blank=True)
|
||||
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)
|
||||
|
||||
@@ -120,6 +192,7 @@ class CartItem(models.Model):
|
||||
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):
|
||||
@@ -130,23 +203,69 @@ class CartItem(models.Model):
|
||||
return 0
|
||||
|
||||
class Bounty(models.Model):
|
||||
card = models.ForeignKey(Card, on_delete=models.CASCADE, related_name='bounties')
|
||||
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):
|
||||
return f"WANTED: {self.card.name} @ ${self.target_price}"
|
||||
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):
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='vault_items')
|
||||
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 = ('user', 'card')
|
||||
unique_together = ('buyer', 'card')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username}'s {self.card.name} ({self.quantity})"
|
||||
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()}"
|
||||
|
||||
Reference in New Issue
Block a user