data model change and some new services
This commit is contained in:
parent
37a5dac06a
commit
85510a4671
3
.gitignore
vendored
3
.gitignore
vendored
@ -169,4 +169,5 @@ cython_debug/
|
|||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
# my stuff
|
# my stuff
|
||||||
*.db
|
*.db
|
||||||
|
temp/
|
381
db/models.py
381
db/models.py
@ -1,31 +1,353 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, ForeignKey
|
from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, ForeignKey
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship, validates
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
class Box(Base):
|
## Core Models
|
||||||
__tablename__ = "boxes"
|
|
||||||
|
|
||||||
id = Column(String, primary_key=True, index=True)
|
class Product(Base):
|
||||||
upload_id = Column(String, ForeignKey("upload_history.upload_id"))
|
"""
|
||||||
|
product is the concept of a physical item that can be sold
|
||||||
|
"""
|
||||||
|
__tablename__ = "products"
|
||||||
|
|
||||||
|
@validates("type")
|
||||||
|
def validate_type(self, key, type: str):
|
||||||
|
if type not in ProductTypeEnum or type.lower() not in ProductTypeEnum:
|
||||||
|
raise ValueError(f"Invalid product type: {type}")
|
||||||
|
return type
|
||||||
|
|
||||||
|
@validates("product_line")
|
||||||
|
def validate_product_line(self, key, product_line: str):
|
||||||
|
if product_line not in ProductLineEnum or product_line.lower() not in ProductLineEnum:
|
||||||
|
raise ValueError(f"Invalid product line: {product_line}")
|
||||||
|
return product_line
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
name = Column(String)
|
||||||
|
type = Column(String) # box or card
|
||||||
|
product_line = Column(String) # pokemon, mtg, etc.
|
||||||
set_name = Column(String)
|
set_name = Column(String)
|
||||||
set_code = Column(String)
|
set_code = Column(String)
|
||||||
type = Column(String)
|
|
||||||
cost = Column(Float)
|
|
||||||
date_purchased = Column(DateTime)
|
|
||||||
date_opened = Column(DateTime)
|
|
||||||
date_created = Column(DateTime, default=datetime.now)
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class Sale(Base):
|
||||||
|
"""
|
||||||
|
sale represents a transaction where a product was sold to a customer on a marketplace
|
||||||
|
"""
|
||||||
|
__tablename__ = "sales"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
ledger_id = Column(String, ForeignKey("ledgers.id"))
|
||||||
|
customer_id = Column(String, ForeignKey("customers.id"))
|
||||||
|
marketplace_id = Column(String, ForeignKey("marketplaces.id"))
|
||||||
|
amount = Column(Float)
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class Ledger(Base):
|
||||||
|
"""
|
||||||
|
ledger associates financial transactions with a user
|
||||||
|
"""
|
||||||
|
__tablename__ = "ledgers"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
user_id = Column(String, ForeignKey("users.id"))
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class Expense(Base):
|
||||||
|
"""
|
||||||
|
expense is any cash outflow associated with moving a product
|
||||||
|
can be optionally associated with a sale or a product
|
||||||
|
"""
|
||||||
|
__tablename__ = "expenses"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
ledger_id = Column(String, ForeignKey("ledgers.id"))
|
||||||
|
product_id = Column(String, ForeignKey("products.id"), nullable=True)
|
||||||
|
sale_id = Column(String, ForeignKey("sales.id"), nullable=True)
|
||||||
|
cost = Column(Float)
|
||||||
|
type = Column(String) # price paid, cogs, shipping, refund, supplies, subscription, fee, etc.
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class Marketplace(Base):
|
||||||
|
"""
|
||||||
|
Marketplace represents a marketplace where products can be sold
|
||||||
|
"""
|
||||||
|
__tablename__ = "marketplaces"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class Box(Base):
|
||||||
|
"""
|
||||||
|
Box Represents a physical product with a sku that contains trading cards
|
||||||
|
Boxes can be sealed or opened
|
||||||
|
Opened boxes have cards associated with them
|
||||||
|
A box contains cards regardless of the inventory status of those cards
|
||||||
|
"""
|
||||||
|
__tablename__ = "boxes"
|
||||||
|
|
||||||
|
@validates("type")
|
||||||
|
def validate_type(self, key, type: str):
|
||||||
|
if type not in BoxTypeEnum or type.lower() not in BoxTypeEnum:
|
||||||
|
raise ValueError(f"Invalid box type: {type}")
|
||||||
|
return type
|
||||||
|
|
||||||
|
product_id = Column(String, ForeignKey("products.id"), primary_key=True)
|
||||||
|
type = Column(String) # collector box, play box, etc.
|
||||||
|
sku = Column(String)
|
||||||
|
num_cards_expected = Column(Integer, nullable=True)
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class OpenBox(Base):
|
||||||
|
__tablename__ = "open_boxes"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
product_id = Column(String, ForeignKey("products.id"))
|
||||||
|
num_cards_actual = Column(Integer)
|
||||||
|
date_opened = Column(DateTime, default=datetime.now)
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class Card(Base):
|
||||||
|
"""
|
||||||
|
Card represents the concept of a distinct card
|
||||||
|
Cards have metadata from different sources
|
||||||
|
internal: box, inventory, upload
|
||||||
|
external: price, attributes - scryfall, tcgplayer, manabox
|
||||||
|
"""
|
||||||
|
__tablename__ = "cards"
|
||||||
|
|
||||||
|
@validates("rarity")
|
||||||
|
def validate_rarity(self, key, rarity: str):
|
||||||
|
single_character_rarity = {'m': 'mythic', 'r': 'rare', 'u': 'uncommon', 'c': 'common', 'l': 'land', 'p': 'promo', 's': 'special'}
|
||||||
|
if rarity not in RarityEnum:
|
||||||
|
if rarity.lower() in RarityEnum:
|
||||||
|
rarity = rarity.lower()
|
||||||
|
elif rarity in single_character_rarity:
|
||||||
|
rarity = single_character_rarity[rarity]
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid rarity: {rarity}")
|
||||||
|
return rarity
|
||||||
|
|
||||||
|
@validates("condition")
|
||||||
|
def validate_condition(self, key, condition: str):
|
||||||
|
if condition not in ConditionEnum:
|
||||||
|
if condition.lower() in ConditionEnum:
|
||||||
|
condition = condition.lower()
|
||||||
|
elif condition.lower().strip().replace(' ', '_') in ConditionEnum:
|
||||||
|
condition = condition.lower().strip().replace(' ', '_')
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid condition: {condition}")
|
||||||
|
return condition
|
||||||
|
|
||||||
|
product_id = Column(String, ForeignKey("products.id"), primary_key=True)
|
||||||
|
number = Column(String)
|
||||||
|
foil = Column(String)
|
||||||
|
rarity = Column(String)
|
||||||
|
condition = Column(String)
|
||||||
|
language = Column(String)
|
||||||
|
scryfall_id = Column(String)
|
||||||
|
manabox_id = Column(String)
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class CardManabox(Base):
|
||||||
|
__tablename__ = "card_manabox"
|
||||||
|
|
||||||
|
product_id = Column(String, ForeignKey("cards.product_id"), primary_key=True)
|
||||||
|
name = Column(String)
|
||||||
|
set_code = Column(String)
|
||||||
|
set_name = Column(String)
|
||||||
|
collector_number = Column(String)
|
||||||
|
foil = Column(String)
|
||||||
|
rarity = Column(String)
|
||||||
|
manabox_id = Column(String)
|
||||||
|
scryfall_id = Column(String)
|
||||||
|
condition = Column(String)
|
||||||
|
language = Column(String)
|
||||||
|
|
||||||
|
class Warehouse(Base):
|
||||||
|
"""
|
||||||
|
container that is associated with a user and contains inventory and stock
|
||||||
|
"""
|
||||||
|
__tablename__ = "warehouse"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
user_id = Column(String, ForeignKey("users.id"), default="admin")
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class Stock(Base):
|
||||||
|
"""
|
||||||
|
contains products that are listed for sale
|
||||||
|
"""
|
||||||
|
__tablename__ = "stock"
|
||||||
|
|
||||||
|
product_id = Column(String, ForeignKey("products.id"), primary_key=True)
|
||||||
|
warehouse_id = Column(String, ForeignKey("warehouse.id"), default="default")
|
||||||
|
marketplace_id = Column(String, ForeignKey("marketplaces.id"))
|
||||||
|
quantity = Column(Integer)
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class Inventory(Base):
|
||||||
|
"""
|
||||||
|
contains products in inventory (not necessarily listed for sale)
|
||||||
|
sealed product in breakdown queue, held sealed product, speculatively held singles, etc.
|
||||||
|
inventory can contain products across multiple marketplaces
|
||||||
|
"""
|
||||||
|
__tablename__ = "inventory"
|
||||||
|
|
||||||
|
product_id = Column(String, ForeignKey("products.id"), primary_key=True)
|
||||||
|
warehouse_id = Column(String, ForeignKey("warehouse.id"), default="default")
|
||||||
|
quantity = Column(Integer)
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
"""
|
||||||
|
User represents a user in the system
|
||||||
|
"""
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class Customer(Base):
|
||||||
|
"""
|
||||||
|
Customer represents a customer that has purchased at least 1 product
|
||||||
|
"""
|
||||||
|
__tablename__ = "customers"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class StagedFileProduct(Base):
|
||||||
|
__tablename__ = "staged_file_products"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
product_id = Column(String, ForeignKey("products.id"))
|
||||||
|
file_id = Column(String, ForeignKey("files.id"))
|
||||||
|
quantity = Column(Integer)
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class File(Base):
|
||||||
|
"""
|
||||||
|
File represents a file that has been uploaded to or retrieved by the system
|
||||||
|
"""
|
||||||
|
__tablename__ = "files"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
type = Column(String) # upload, export, etc.
|
||||||
|
source = Column(String) # manabox, tcgplayer, etc.
|
||||||
|
service = Column(String) # pricing, data, etc.
|
||||||
|
filename = Column(String)
|
||||||
|
filepath = Column(String) # backup location
|
||||||
|
filesize_kb = Column(Float)
|
||||||
|
status = Column(String)
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class Price(Base):
|
||||||
|
__tablename__ = "price"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
product_id = Column(String, ForeignKey("products.id"))
|
||||||
|
marketplace_id = Column(String, ForeignKey("marketplaces.id"))
|
||||||
|
type = Column(String) # market, direct, low, low_with_shipping, marketplace
|
||||||
|
price = Column(Float)
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class StorageBlock(Base):
|
||||||
|
"""
|
||||||
|
StorageBlock represents a physical storage location for products (50 card indexed block in a box)
|
||||||
|
"""
|
||||||
|
__tablename__ = "storage_blocks"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
warehouse_id = Column(String, ForeignKey("warehouse.id"))
|
||||||
|
name = Column(String)
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
|
||||||
|
## Relationships
|
||||||
|
|
||||||
|
class ProductBlock(Base):
|
||||||
|
"""
|
||||||
|
ProductBlock represents the relationship between a product and a storage block
|
||||||
|
which products are in a block and at what index
|
||||||
|
"""
|
||||||
|
__tablename__ = "product_block"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
product_id = Column(String, ForeignKey("products.id"))
|
||||||
|
block_id = Column(String, ForeignKey("storage_blocks.id"))
|
||||||
|
block_index = Column(Integer)
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class ProductMarketPrice(Base):
|
||||||
|
__tablename__ = "product_market_price"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
product_id = Column(String, ForeignKey("products.id"))
|
||||||
|
marketplace_id = Column(String, ForeignKey("marketplaces.id"))
|
||||||
|
price_id = Column(String, ForeignKey("price.id"))
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class OpenBoxCard(Base):
|
||||||
|
"""
|
||||||
|
OpenedBoxCard represents the relationship between an opened box and the cards it contains
|
||||||
|
"""
|
||||||
|
__tablename__ = "open_box_card"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
open_box_id = Column(String, ForeignKey("open_boxes.id"))
|
||||||
|
card_id = Column(String, ForeignKey("cards.product_id"))
|
||||||
|
quantity = Column(Integer)
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
class ProductSale(Base):
|
||||||
|
"""
|
||||||
|
ProductSale represents the relationship between products and sales
|
||||||
|
"""
|
||||||
|
__tablename__ = "product_sale"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
product_id = Column(String, ForeignKey("products.id"))
|
||||||
|
sale_id = Column(String, ForeignKey("sales.id"))
|
||||||
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
## older
|
||||||
|
|
||||||
class ManaboxExportData(Base):
|
class ManaboxExportData(Base):
|
||||||
__tablename__ = "manabox_export_data"
|
__tablename__ = "manabox_export_data"
|
||||||
|
|
||||||
id = Column(String, primary_key=True)
|
id = Column(String, primary_key=True)
|
||||||
upload_id = Column(String)
|
# upload_id = Column(String)
|
||||||
box_id = Column(String, nullable=True)
|
# box_id = Column(String, nullable=True)
|
||||||
name = Column(String)
|
name = Column(String)
|
||||||
set_code = Column(String)
|
set_code = Column(String)
|
||||||
set_name = Column(String)
|
set_name = Column(String)
|
||||||
@ -50,6 +372,8 @@ class UploadHistory(Base):
|
|||||||
id = Column(String, primary_key=True)
|
id = Column(String, primary_key=True)
|
||||||
upload_id = Column(String)
|
upload_id = Column(String)
|
||||||
filename = Column(String)
|
filename = Column(String)
|
||||||
|
file_size_kb = Column(Float)
|
||||||
|
num_rows = Column(Integer)
|
||||||
date_created = Column(DateTime, default=datetime.now)
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
status = Column(String)
|
status = Column(String)
|
||||||
@ -159,4 +483,37 @@ class UnmatchedManaboxData(Base):
|
|||||||
manabox_id = Column(String, ForeignKey("manabox_export_data.id"))
|
manabox_id = Column(String, ForeignKey("manabox_export_data.id"))
|
||||||
reason = Column(String)
|
reason = Column(String)
|
||||||
date_created = Column(DateTime, default=datetime.now)
|
date_created = Column(DateTime, default=datetime.now)
|
||||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
# enums
|
||||||
|
|
||||||
|
class RarityEnum(str, Enum):
|
||||||
|
COMMON = "common"
|
||||||
|
UNCOMMON = "uncommon"
|
||||||
|
RARE = "rare"
|
||||||
|
MYTHIC = "mythic"
|
||||||
|
LAND = "land"
|
||||||
|
PROMO = "promo"
|
||||||
|
SPECIAL = "special"
|
||||||
|
|
||||||
|
class ConditionEnum(str, Enum):
|
||||||
|
MINT = "mint"
|
||||||
|
NEAR_MINT = "near_mint"
|
||||||
|
LIGHTLY_PLAYED = "lightly_played"
|
||||||
|
MODERATELY_PLAYED = "moderately_played"
|
||||||
|
HEAVILY_PLAYED = "heavily_played"
|
||||||
|
DAMAGED = "damaged"
|
||||||
|
|
||||||
|
class BoxTypeEnum(str, Enum):
|
||||||
|
COLLECTOR = "collector"
|
||||||
|
PLAY = "play"
|
||||||
|
DRAFT = "draft"
|
||||||
|
COMMANDER = "commander"
|
||||||
|
|
||||||
|
class ProductLineEnum(str, Enum):
|
||||||
|
MTG = "mtg"
|
||||||
|
POKEMON = "pokemon"
|
||||||
|
|
||||||
|
class ProductTypeEnum(str, Enum):
|
||||||
|
BOX = "box"
|
||||||
|
CARD = "card"
|
@ -4,10 +4,39 @@ from services.upload import UploadService
|
|||||||
from services.box import BoxService
|
from services.box import BoxService
|
||||||
from services.tcgplayer import TCGPlayerService
|
from services.tcgplayer import TCGPlayerService
|
||||||
from services.pricing import PricingService
|
from services.pricing import PricingService
|
||||||
from fastapi import Depends
|
from services.file import FileService
|
||||||
|
from services.product import ProductService
|
||||||
|
from services.inventory import InventoryService
|
||||||
|
from fastapi import Depends, Form
|
||||||
from db.database import get_db
|
from db.database import get_db
|
||||||
|
from schemas.file import FileMetadata
|
||||||
|
|
||||||
|
|
||||||
|
## file
|
||||||
|
# file
|
||||||
|
def get_file_service(db: Session = Depends(get_db)) -> FileService:
|
||||||
|
"""Dependency injection for FileService"""
|
||||||
|
return FileService(db)
|
||||||
|
|
||||||
|
# metadata
|
||||||
|
def get_file_metadata(
|
||||||
|
type: str = Form(...),
|
||||||
|
source: str = Form(...)
|
||||||
|
) -> FileMetadata:
|
||||||
|
"""Dependency injection for FileMetadata"""
|
||||||
|
return FileMetadata(type=type, source=source)
|
||||||
|
|
||||||
|
# product
|
||||||
|
def get_product_service(db: Session = Depends(get_db), file_service: FileService = Depends(get_file_service)) -> ProductService:
|
||||||
|
"""Dependency injection for ProductService"""
|
||||||
|
return ProductService(db, file_service)
|
||||||
|
|
||||||
|
|
||||||
|
## Inventory
|
||||||
|
def get_inventory_service(db: Session = Depends(get_db)) -> InventoryService:
|
||||||
|
"""Dependency injection for InventoryService"""
|
||||||
|
return InventoryService(db)
|
||||||
|
|
||||||
## Upload
|
## Upload
|
||||||
|
|
||||||
def get_upload_service(db: Session = Depends(get_db)) -> UploadService:
|
def get_upload_service(db: Session = Depends(get_db)) -> UploadService:
|
||||||
@ -16,9 +45,9 @@ def get_upload_service(db: Session = Depends(get_db)) -> UploadService:
|
|||||||
|
|
||||||
## box
|
## box
|
||||||
|
|
||||||
def get_box_service(db: Session = Depends(get_db)) -> BoxService:
|
def get_box_service(db: Session = Depends(get_db), inventory_service: InventoryService = Depends(get_inventory_service)) -> BoxService:
|
||||||
"""Dependency injection for BoxService"""
|
"""Dependency injection for BoxService"""
|
||||||
return BoxService(db)
|
return BoxService(db, inventory_service)
|
||||||
|
|
||||||
## Pricing
|
## Pricing
|
||||||
|
|
||||||
|
@ -7,7 +7,11 @@ from services.upload import UploadService
|
|||||||
from services.box import BoxService
|
from services.box import BoxService
|
||||||
from services.tcgplayer import TCGPlayerService
|
from services.tcgplayer import TCGPlayerService
|
||||||
from services.data import DataService
|
from services.data import DataService
|
||||||
from dependencies import get_data_service, get_upload_service, get_tcgplayer_service, get_box_service
|
from services.file import FileService
|
||||||
|
from services.product import ProductService
|
||||||
|
from schemas.file import FileMetadata, FileUploadResponse, GetPreparedFilesResponse, FileDeleteResponse
|
||||||
|
from schemas.box import CreateBoxResponse, CreateBoxRequestData
|
||||||
|
from dependencies import get_data_service, get_upload_service, get_tcgplayer_service, get_box_service, get_file_metadata, get_file_service, get_product_service
|
||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@ -15,30 +19,55 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
router = APIRouter(prefix="/api", tags=["cards"])
|
router = APIRouter(prefix="/api", tags=["cards"])
|
||||||
|
|
||||||
## health check
|
|
||||||
@router.get("/health", response_model=dict)
|
|
||||||
async def health_check() -> dict:
|
|
||||||
"""
|
|
||||||
Health check endpoint
|
|
||||||
"""
|
|
||||||
logger.info("Health check")
|
|
||||||
return {"status": "ok"}
|
|
||||||
|
|
||||||
## test endpoint - logs all detail about request
|
# FILE
|
||||||
@router.post("/test", response_model=dict)
|
@router.post("/file/uploadManabox", response_model=FileUploadResponse)
|
||||||
async def test_endpoint(request: Request, file:UploadFile = File(...)) -> dict:
|
async def upload_file(
|
||||||
"""
|
background_tasks: BackgroundTasks,
|
||||||
Test endpoint
|
file: UploadFile = File(...),
|
||||||
"""
|
file_service: FileService = Depends(get_file_service),
|
||||||
content = await file.read()
|
product_service: ProductService = Depends(get_product_service),
|
||||||
# log filename
|
metadata: FileMetadata = Depends(get_file_metadata)) -> FileUploadResponse:
|
||||||
logger.info(f"file received: {file.filename}")
|
try:
|
||||||
# print first 100 characters of file content
|
content = await file.read()
|
||||||
logger.info(f"file content: {content[:100]}")
|
metadata.service = 'product'
|
||||||
|
result = file_service.upload_file(content, file.filename, metadata)
|
||||||
|
background_tasks.add_task(product_service.bg_process_manabox_file, result.id)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"File upload failed: {str(e)}")
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
return {"status": "ok"}
|
|
||||||
|
@router.get("/file/getPreparedFiles", response_model=GetPreparedFilesResponse)
|
||||||
|
async def get_prepared_files(file_service: FileService = Depends(get_file_service)) -> GetPreparedFilesResponse:
|
||||||
|
try:
|
||||||
|
response = file_service.get_prepared_files()
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Get prepared files failed: {str(e)}")
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
@router.post("/file/deleteFile", response_model=FileDeleteResponse)
|
||||||
|
async def delete_file(file_id: str, file_service: FileService = Depends(get_file_service)) -> FileDeleteResponse:
|
||||||
|
try:
|
||||||
|
response = file_service.delete_file(file_id)
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Delete file failed: {str(e)}")
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
@router.post("/box/createBox", response_model=CreateBoxResponse)
|
||||||
|
async def create_box(file_ids: list[str], create_box_data: CreateBoxRequestData, box_service: BoxService = Depends(get_box_service)) -> CreateBoxResponse:
|
||||||
|
try:
|
||||||
|
create_box_data = create_box_data.dict()
|
||||||
|
response = box_service.create_box(create_box_data, file_ids)
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Create box failed: {str(e)}")
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
## all old below
|
||||||
|
|
||||||
@router.post("/upload/manabox", response_model=dict)
|
@router.post("/upload/manabox", response_model=dict)
|
||||||
async def upload_manabox(
|
async def upload_manabox(
|
||||||
@ -55,6 +84,8 @@ async def upload_manabox(
|
|||||||
# Read the file content
|
# Read the file content
|
||||||
content = await file.read()
|
content = await file.read()
|
||||||
filename = file.filename
|
filename = file.filename
|
||||||
|
file_size = len(content)
|
||||||
|
file_size_kb = file_size / 1024
|
||||||
if not content:
|
if not content:
|
||||||
logger.error("Empty file content")
|
logger.error("Empty file content")
|
||||||
raise HTTPException(status_code=400, detail="Empty file content")
|
raise HTTPException(status_code=400, detail="Empty file content")
|
||||||
@ -64,7 +95,7 @@ async def upload_manabox(
|
|||||||
logger.error("File must be a CSV")
|
logger.error("File must be a CSV")
|
||||||
raise HTTPException(status_code=400, detail="File must be a CSV")
|
raise HTTPException(status_code=400, detail="File must be a CSV")
|
||||||
|
|
||||||
result = upload_service.process_manabox_upload(content, filename)
|
result = upload_service.process_manabox_upload(content, filename, file_size_kb)
|
||||||
background_tasks.add_task(data_service.bg_set_manabox_tcg_relationship, upload_id=result[1])
|
background_tasks.add_task(data_service.bg_set_manabox_tcg_relationship, upload_id=result[1])
|
||||||
return result[0]
|
return result[0]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
0
schemas/__init__.py
Normal file
0
schemas/__init__.py
Normal file
13
schemas/base.py
Normal file
13
schemas/base.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from pydantic import BaseModel, Field, validator
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
|
||||||
|
# Base schemas with shared attributes
|
||||||
|
class BaseSchema(BaseModel):
|
||||||
|
date_created: datetime
|
||||||
|
date_modified: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True # Allows conversion from SQLAlchemy models
|
25
schemas/box.py
Normal file
25
schemas/box.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from schemas.base import BaseSchema
|
||||||
|
|
||||||
|
class CreateBoxResponse(BaseModel):
|
||||||
|
success: bool = Field(..., title="Success")
|
||||||
|
|
||||||
|
class CreateOpenBoxResponse(CreateBoxResponse):
|
||||||
|
open_box_id: str
|
||||||
|
|
||||||
|
class CreateSealedBoxResponse(CreateBoxResponse):
|
||||||
|
product_id: str
|
||||||
|
quantity: int
|
||||||
|
|
||||||
|
class CreateBoxRequestData(BaseModel):
|
||||||
|
type: str = Field(..., title="Box Type (collector, play, draft)")
|
||||||
|
name: str = Field(..., title="Name")
|
||||||
|
product_line: str = Field(..., title="Product Line (MTG, Pokemon, etc)")
|
||||||
|
set_name: str = Field(..., title="Set Name")
|
||||||
|
set_code: str = Field(..., title="Set Code")
|
||||||
|
sealed: bool = Field(..., title="Sealed: Boolean")
|
||||||
|
sku: str = Field(..., title="SKU")
|
||||||
|
num_cards_expected: int = Field(..., title="Number of cards expected")
|
||||||
|
num_cards_actual: int = Field(None, title="Number of cards actual")
|
||||||
|
date_purchased: str = Field(..., title="Date purchased")
|
||||||
|
date_opened: str = Field(None, title="Date opened")
|
29
schemas/file.py
Normal file
29
schemas/file.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
from schemas.base import BaseSchema
|
||||||
|
from fastapi import UploadFile
|
||||||
|
|
||||||
|
# For additional metadata about the upload
|
||||||
|
class FileMetadata(BaseModel):
|
||||||
|
source: str = Field(..., title="Source")
|
||||||
|
type: str = Field(..., title="Type")
|
||||||
|
# optional
|
||||||
|
service: Optional[str] = Field(None, title="Service")
|
||||||
|
|
||||||
|
# For the response after upload
|
||||||
|
class FileUploadResponse(BaseSchema):
|
||||||
|
id: str
|
||||||
|
filename: str
|
||||||
|
type: str
|
||||||
|
file_size_kb: float
|
||||||
|
source: str
|
||||||
|
status: str
|
||||||
|
service: str
|
||||||
|
|
||||||
|
class FileDeleteResponse(BaseModel):
|
||||||
|
id: str
|
||||||
|
status: str
|
||||||
|
|
||||||
|
class GetPreparedFilesResponse(BaseModel):
|
||||||
|
files: list[FileUploadResponse]
|
6
schemas/inventory.py
Normal file
6
schemas/inventory.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from schemas.base import BaseSchema
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateInventoryResponse(BaseModel):
|
||||||
|
success: bool = Field(..., title="Success")
|
211
services/box.py
211
services/box.py
@ -1,100 +1,135 @@
|
|||||||
from db.models import ManaboxExportData, Box, UploadHistory
|
from db.models import Box, File, StagedFileProduct, Product, OpenBoxCard, OpenBox, Inventory
|
||||||
from db.utils import db_transaction
|
from db.utils import db_transaction
|
||||||
import uuid
|
from uuid import uuid4 as uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy.engine.result import Row
|
from sqlalchemy.engine.result import Row
|
||||||
|
from schemas.box import CreateOpenBoxResponse, CreateSealedBoxResponse, CreateBoxResponse
|
||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
from db.utils import db_transaction
|
||||||
|
from services.inventory import InventoryService
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class BoxObject:
|
|
||||||
def __init__(
|
|
||||||
self, upload_id: str, set_name: str,
|
|
||||||
set_code: str, cost: float = None, date_purchased: datetime = None,
|
|
||||||
date_opened: datetime = None, box_id: str = None):
|
|
||||||
self.upload_id = upload_id
|
|
||||||
self.box_id = box_id if box_id else str(uuid.uuid4())
|
|
||||||
self.set_name = set_name
|
|
||||||
self.set_code = set_code
|
|
||||||
self.cost = cost
|
|
||||||
self.date_purchased = date_purchased
|
|
||||||
self.date_opened = date_opened
|
|
||||||
|
|
||||||
class BoxService:
|
class BoxService:
|
||||||
def __init__(self, db: Session):
|
def __init__(self, db: Session, inventory_service: InventoryService):
|
||||||
self.db = db
|
self.db = db
|
||||||
|
self.inventory_service = inventory_service
|
||||||
def _validate_upload_id(self, upload_id: str):
|
|
||||||
# check if upload_history status = 'success'
|
|
||||||
if self.db.query(UploadHistory).filter(UploadHistory.upload_id == upload_id).first() is None:
|
|
||||||
raise Exception(f"Upload ID {upload_id} not found")
|
|
||||||
if self.db.query(UploadHistory).filter(UploadHistory.upload_id == upload_id).first().status != 'success':
|
|
||||||
raise Exception(f"Upload ID {upload_id} not successful")
|
|
||||||
# check if at least 1 row in manabox_export_data with upload_id
|
|
||||||
if self.db.query(ManaboxExportData).filter(ManaboxExportData.upload_id == upload_id).first() is None:
|
|
||||||
raise Exception(f"Upload ID {upload_id} has no data")
|
|
||||||
|
|
||||||
def _get_set_info(self, upload_id: str) -> list[Row[tuple[str, str]]]:
|
|
||||||
# get distinct set_name, set_code from manabox_export_data for upload_id
|
|
||||||
boxes = self.db.query(ManaboxExportData.set_name, ManaboxExportData.set_code).filter(ManaboxExportData.upload_id == upload_id).distinct().all()
|
|
||||||
if not boxes or len(boxes) == 0:
|
|
||||||
raise Exception(f"Upload ID {upload_id} has no data")
|
|
||||||
return boxes
|
|
||||||
|
|
||||||
def _update_manabox_export_data_box_id(self, box: Box):
|
|
||||||
# based on upload_id, set_name, set_code, update box_id in manabox_export_data for all rows where box id is null
|
|
||||||
with db_transaction(self.db):
|
|
||||||
self.db.query(ManaboxExportData).filter(
|
|
||||||
ManaboxExportData.upload_id == box.upload_id).filter(
|
|
||||||
ManaboxExportData.set_name == box.set_name).filter(
|
|
||||||
ManaboxExportData.set_code == box.set_code).filter(
|
|
||||||
ManaboxExportData.box_id == None).update({ManaboxExportData.box_id: box.id})
|
|
||||||
|
|
||||||
def convert_upload_to_boxes(self, upload_id: str):
|
def validate_file_ids(self, file_ids: list[str]):
|
||||||
self._validate_upload_id(upload_id)
|
# check if all file_ids are valid
|
||||||
# get distinct set_name, set_code from manabox_export_data for upload_id
|
for file_id in file_ids:
|
||||||
box_set_info = self._get_set_info(upload_id)
|
if self.db.query(File).filter(File.id == file_id).first() is None:
|
||||||
created_boxes = []
|
raise Exception(f"File ID {file_id} not found")
|
||||||
# create boxes
|
|
||||||
for box in box_set_info:
|
def get_staged_product_data(self, file_ids: list[str]) -> StagedFileProduct:
|
||||||
box_obj = BoxObject(upload_id, set_name = box.set_name, set_code = box.set_code)
|
staged_product_data = self.db.query(StagedFileProduct).filter(StagedFileProduct.file_id.in_(file_ids)).all()
|
||||||
new_box = self.create_box(box_obj)
|
return staged_product_data
|
||||||
logger.info(f"Created box {new_box.id} for upload {upload_id}")
|
|
||||||
self._update_manabox_export_data_box_id(new_box)
|
def aggregate_staged_product_data(self, staged_product_data: list[Row]) -> dict[Product, int]:
|
||||||
created_boxes.append(new_box)
|
product_data = {}
|
||||||
|
for row in staged_product_data:
|
||||||
|
product = self.db.query(Product).filter(Product.id == row.product_id).first()
|
||||||
|
if product not in product_data:
|
||||||
|
product_data[product] = 0
|
||||||
|
product_data[product] += row.quantity
|
||||||
|
return product_data
|
||||||
|
|
||||||
|
def find_product_for_box_data(self, create_box_data: dict[str, Any]) -> Product:
|
||||||
|
existing_product = self.db.query(Product).filter(
|
||||||
|
Product.name == create_box_data["name"], # TODO: needs complex enum
|
||||||
|
Product.type == "box",
|
||||||
|
Product.set_code == create_box_data["set_code"], # TODO: needs complex enum
|
||||||
|
Product.set_name == create_box_data["set_name"], # TODO: needs complex enum
|
||||||
|
Product.product_line == create_box_data["product_line"]).first()
|
||||||
|
return existing_product
|
||||||
|
|
||||||
|
def create_product_for_box(self, create_box_data: dict[str, Any]) -> Product:
|
||||||
|
product = Product(
|
||||||
|
id=str(uuid()),
|
||||||
|
name=create_box_data["name"],
|
||||||
|
type="box",
|
||||||
|
set_code=create_box_data["set_code"],
|
||||||
|
set_name=create_box_data["set_name"],
|
||||||
|
product_line=create_box_data["product_line"]
|
||||||
|
)
|
||||||
|
self.db.add(product)
|
||||||
|
return product
|
||||||
|
|
||||||
|
def create_box_db(self, product: Product, create_box_data: dict[str, Any]) -> Box:
|
||||||
|
box = Box(
|
||||||
|
product_id=product.id,
|
||||||
|
type=create_box_data["type"],
|
||||||
|
sku=create_box_data["sku"],
|
||||||
|
num_cards_expected=create_box_data["num_cards_expected"]
|
||||||
|
)
|
||||||
|
self.db.add(box)
|
||||||
|
return box
|
||||||
|
|
||||||
|
def create_open_box(self, product: Product, create_box_data: dict[str, Any]) -> OpenBox:
|
||||||
|
open_box = OpenBox(
|
||||||
|
id = str(uuid()),
|
||||||
|
product_id=product.id,
|
||||||
|
num_cards_actual=create_box_data["num_cards_actual"],
|
||||||
|
date_opened=datetime.strptime(create_box_data["date_opened"], "%Y-%m-%d")
|
||||||
|
)
|
||||||
|
self.db.add(open_box)
|
||||||
|
return open_box
|
||||||
|
|
||||||
|
def add_products_to_open_box(self, open_box: OpenBox, product_data: dict[Product, int]) -> None:
|
||||||
|
for product, quantity in product_data.items():
|
||||||
|
open_box_card = OpenBoxCard(
|
||||||
|
id=str(uuid()),
|
||||||
|
open_box_id=open_box.id,
|
||||||
|
card_id=product.id,
|
||||||
|
quantity=quantity
|
||||||
|
)
|
||||||
|
self.db.add(open_box_card)
|
||||||
|
|
||||||
|
def format_response(self, open_box: OpenBox = None, inventory: Inventory = None) -> CreateBoxResponse:
|
||||||
|
response = CreateBoxResponse(success=True)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def create_box(self, create_box_data: dict[str, Any], file_ids: list[str] = None) -> CreateBoxResponse:
|
||||||
|
sealed = create_box_data["sealed"]
|
||||||
|
assert isinstance(sealed, bool)
|
||||||
|
if file_ids and not sealed:
|
||||||
|
self.validate_file_ids(file_ids)
|
||||||
|
staged_product_data = self.get_staged_product_data(file_ids)
|
||||||
|
product_data = self.aggregate_staged_product_data(staged_product_data)
|
||||||
|
elif file_ids and sealed:
|
||||||
|
raise Exception("Cannot add cards with a sealed box")
|
||||||
|
|
||||||
return {"status": "success", "boxes": f"{[box.id for box in created_boxes]}"}
|
# find product with all same box data
|
||||||
|
existing_product = self.find_product_for_box_data(create_box_data)
|
||||||
|
|
||||||
|
if existing_product:
|
||||||
|
box_product = existing_product
|
||||||
|
|
||||||
|
try:
|
||||||
|
with db_transaction(self.db):
|
||||||
|
if not existing_product:
|
||||||
|
box_product = self.create_product_for_box(create_box_data)
|
||||||
|
box = self.create_box_db(box_product, create_box_data)
|
||||||
|
if not sealed:
|
||||||
|
open_box = self.create_open_box(box_product, create_box_data)
|
||||||
|
if file_ids:
|
||||||
|
process_staged_products = self.inventory_service.process_staged_products(product_data)
|
||||||
|
self.add_products_to_open_box(open_box, product_data)
|
||||||
|
# should be the file service handling this but im about to die irl
|
||||||
|
# update file id status to processed
|
||||||
|
for file_id in file_ids:
|
||||||
|
file = self.db.query(File).filter(File.id == file_id).first()
|
||||||
|
file.status = "processed"
|
||||||
|
self.db.add(file)
|
||||||
|
return self.format_response(open_box=open_box)
|
||||||
|
elif not file_ids and sealed:
|
||||||
|
# add sealed box to inventory
|
||||||
|
inventory = self.inventory_service.add_sealed_box_to_inventory(box_product, 1)
|
||||||
|
return self.format_response(inventory=inventory)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
def create_box(self, box: BoxObject):
|
logger.error(f"Error creating box: {str(e)}")
|
||||||
with db_transaction(self.db):
|
raise e
|
||||||
box_record = Box(
|
|
||||||
id = box.box_id,
|
|
||||||
upload_id = box.upload_id,
|
|
||||||
set_name = box.set_name,
|
|
||||||
set_code = box.set_code,
|
|
||||||
cost = box.cost,
|
|
||||||
date_purchased = box.date_purchased,
|
|
||||||
date_opened = box.date_opened
|
|
||||||
)
|
|
||||||
self.db.add(box_record)
|
|
||||||
return box_record
|
|
||||||
|
|
||||||
def get_box(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def delete_box(self, box_id: str):
|
|
||||||
# delete box
|
|
||||||
with db_transaction(self.db):
|
|
||||||
self.db.query(Box).filter(Box.id == box_id).delete()
|
|
||||||
# update manabox_export_data box_id to null
|
|
||||||
with db_transaction(self.db):
|
|
||||||
self.db.query(ManaboxExportData).filter(ManaboxExportData.box_id == box_id).update({ManaboxExportData.box_id: None})
|
|
||||||
return {"status": "success", "box_id": box_id}
|
|
||||||
|
|
||||||
def update_box(self):
|
|
||||||
pass
|
|
||||||
|
|
75
services/file.py
Normal file
75
services/file.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from db.utils import db_transaction
|
||||||
|
from db.models import File, StagedFileProduct
|
||||||
|
from schemas.file import FileMetadata, FileUploadResponse, GetPreparedFilesResponse, FileDeleteResponse
|
||||||
|
import os
|
||||||
|
from uuid import uuid4 as uuid
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FileService:
|
||||||
|
def __init__(self, db: Session):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def _format_response(self, file: File) -> FileUploadResponse:
|
||||||
|
response = FileUploadResponse(
|
||||||
|
id = file.id,
|
||||||
|
filename = file.filename,
|
||||||
|
type = file.type,
|
||||||
|
source = file.source,
|
||||||
|
status = file.status,
|
||||||
|
service = file.service,
|
||||||
|
file_size_kb = file.filesize_kb,
|
||||||
|
date_created=file.date_created,
|
||||||
|
date_modified=file.date_modified
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def upload_file(self, content: bytes, filename: str, metadata: FileMetadata) -> FileUploadResponse:
|
||||||
|
# Save file to database
|
||||||
|
with db_transaction(self.db):
|
||||||
|
file = File(
|
||||||
|
id = str(uuid()),
|
||||||
|
filename = filename,
|
||||||
|
filepath = os.getcwd() + '/temp/' + filename, # TODO: config variable
|
||||||
|
type = metadata.type,
|
||||||
|
source = metadata.source,
|
||||||
|
filesize_kb = round(len(content) / 1024,2),
|
||||||
|
status = 'pending',
|
||||||
|
service = metadata.service
|
||||||
|
)
|
||||||
|
self.db.add(file)
|
||||||
|
# save file
|
||||||
|
with open(file.filepath, 'wb') as f:
|
||||||
|
f.write(content)
|
||||||
|
response = self._format_response(file)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_file(self, file_id: str) -> File:
|
||||||
|
return self.db.query(File).filter(File.id == file_id).first()
|
||||||
|
|
||||||
|
def get_prepared_files(self) -> list[FileUploadResponse]:
|
||||||
|
files = self.db.query(File).filter(File.status == 'prepared').all()
|
||||||
|
if len(files) == 0:
|
||||||
|
raise Exception("No prepared files found")
|
||||||
|
result = [self._format_response(file) for file in files]
|
||||||
|
logger.debug(f"Prepared files: {result}")
|
||||||
|
response = GetPreparedFilesResponse(files=result)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_staged_products(self, file_id: str) -> list[StagedFileProduct]:
|
||||||
|
return self.db.query(StagedFileProduct).filter(StagedFileProduct.file_id == file_id).all()
|
||||||
|
|
||||||
|
def delete_file(self, file_id: str) -> FileDeleteResponse:
|
||||||
|
file = self.get_file(file_id)
|
||||||
|
staged_products = self.get_staged_products(file_id)
|
||||||
|
if file:
|
||||||
|
with db_transaction(self.db):
|
||||||
|
self.db.delete(file)
|
||||||
|
for staged_product in staged_products:
|
||||||
|
self.db.delete(staged_product)
|
||||||
|
return {"id": file_id, "status": "deleted"}
|
||||||
|
else:
|
||||||
|
raise Exception(f"File with id {file_id} not found")
|
28
services/inventory.py
Normal file
28
services/inventory.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from db.models import Product, Inventory
|
||||||
|
from schemas.inventory import UpdateInventoryResponse
|
||||||
|
from db.utils import db_transaction
|
||||||
|
|
||||||
|
class InventoryService:
|
||||||
|
def __init__(self, db: Session):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def add_inventory(self, product: Product, quantity: int) -> Inventory:
|
||||||
|
inventory = self.db.query(Inventory).filter(Inventory.product_id == product.id).first()
|
||||||
|
if inventory is None:
|
||||||
|
inventory = Inventory(product_id=product.id, quantity=quantity)
|
||||||
|
self.db.add(inventory)
|
||||||
|
else:
|
||||||
|
inventory.quantity += quantity
|
||||||
|
return inventory
|
||||||
|
|
||||||
|
def process_staged_products(self, product_data: dict[Product, int]) -> UpdateInventoryResponse:
|
||||||
|
with db_transaction(self.db):
|
||||||
|
for product, quantity in product_data.items():
|
||||||
|
self.add_inventory(product, quantity)
|
||||||
|
return UpdateInventoryResponse(success=True)
|
||||||
|
|
||||||
|
def add_sealed_box_to_inventory(self, product: Product, quantity: int) -> UpdateInventoryResponse:
|
||||||
|
with db_transaction(self.db):
|
||||||
|
inventory = self.add_inventory(product, quantity)
|
||||||
|
return UpdateInventoryResponse(success=True)
|
100
services/old_box.py
Normal file
100
services/old_box.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
from db.models import ManaboxExportData, Box, UploadHistory
|
||||||
|
from db.utils import db_transaction
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy.engine.result import Row
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class BoxObject:
|
||||||
|
def __init__(
|
||||||
|
self, upload_id: str, set_name: str,
|
||||||
|
set_code: str, cost: float = None, date_purchased: datetime = None,
|
||||||
|
date_opened: datetime = None, box_id: str = None):
|
||||||
|
self.upload_id = upload_id
|
||||||
|
self.box_id = box_id if box_id else str(uuid.uuid4())
|
||||||
|
self.set_name = set_name
|
||||||
|
self.set_code = set_code
|
||||||
|
self.cost = cost
|
||||||
|
self.date_purchased = date_purchased
|
||||||
|
self.date_opened = date_opened
|
||||||
|
|
||||||
|
class BoxService:
|
||||||
|
def __init__(self, db: Session):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def _validate_upload_id(self, upload_id: str):
|
||||||
|
# check if upload_history status = 'success'
|
||||||
|
if self.db.query(UploadHistory).filter(UploadHistory.upload_id == upload_id).first() is None:
|
||||||
|
raise Exception(f"Upload ID {upload_id} not found")
|
||||||
|
if self.db.query(UploadHistory).filter(UploadHistory.upload_id == upload_id).first().status != 'success':
|
||||||
|
raise Exception(f"Upload ID {upload_id} not successful")
|
||||||
|
# check if at least 1 row in manabox_export_data with upload_id
|
||||||
|
if self.db.query(ManaboxExportData).filter(ManaboxExportData.upload_id == upload_id).first() is None:
|
||||||
|
raise Exception(f"Upload ID {upload_id} has no data")
|
||||||
|
|
||||||
|
def _get_set_info(self, upload_id: str) -> list[Row[tuple[str, str]]]:
|
||||||
|
# get distinct set_name, set_code from manabox_export_data for upload_id
|
||||||
|
boxes = self.db.query(ManaboxExportData.set_name, ManaboxExportData.set_code).filter(ManaboxExportData.upload_id == upload_id).distinct().all()
|
||||||
|
if not boxes or len(boxes) == 0:
|
||||||
|
raise Exception(f"Upload ID {upload_id} has no data")
|
||||||
|
return boxes
|
||||||
|
|
||||||
|
def _update_manabox_export_data_box_id(self, box: Box):
|
||||||
|
# based on upload_id, set_name, set_code, update box_id in manabox_export_data for all rows where box id is null
|
||||||
|
with db_transaction(self.db):
|
||||||
|
self.db.query(ManaboxExportData).filter(
|
||||||
|
ManaboxExportData.upload_id == box.upload_id).filter(
|
||||||
|
ManaboxExportData.set_name == box.set_name).filter(
|
||||||
|
ManaboxExportData.set_code == box.set_code).filter(
|
||||||
|
ManaboxExportData.box_id == None).update({ManaboxExportData.box_id: box.id})
|
||||||
|
|
||||||
|
def convert_upload_to_boxes(self, upload_id: str):
|
||||||
|
self._validate_upload_id(upload_id)
|
||||||
|
# get distinct set_name, set_code from manabox_export_data for upload_id
|
||||||
|
box_set_info = self._get_set_info(upload_id)
|
||||||
|
created_boxes = []
|
||||||
|
# create boxes
|
||||||
|
for box in box_set_info:
|
||||||
|
box_obj = BoxObject(upload_id, set_name = box.set_name, set_code = box.set_code)
|
||||||
|
new_box = self.create_box(box_obj)
|
||||||
|
logger.info(f"Created box {new_box.id} for upload {upload_id}")
|
||||||
|
self._update_manabox_export_data_box_id(new_box)
|
||||||
|
created_boxes.append(new_box)
|
||||||
|
|
||||||
|
return {"status": "success", "boxes": f"{[box.id for box in created_boxes]}"}
|
||||||
|
|
||||||
|
|
||||||
|
def create_box(self, box: BoxObject):
|
||||||
|
with db_transaction(self.db):
|
||||||
|
box_record = Box(
|
||||||
|
id = box.box_id,
|
||||||
|
upload_id = box.upload_id,
|
||||||
|
set_name = box.set_name,
|
||||||
|
set_code = box.set_code,
|
||||||
|
cost = box.cost,
|
||||||
|
date_purchased = box.date_purchased,
|
||||||
|
date_opened = box.date_opened
|
||||||
|
)
|
||||||
|
self.db.add(box_record)
|
||||||
|
return box_record
|
||||||
|
|
||||||
|
def get_box(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def delete_box(self, box_id: str):
|
||||||
|
# delete box
|
||||||
|
with db_transaction(self.db):
|
||||||
|
self.db.query(Box).filter(Box.id == box_id).delete()
|
||||||
|
# update manabox_export_data box_id to null
|
||||||
|
with db_transaction(self.db):
|
||||||
|
self.db.query(ManaboxExportData).filter(ManaboxExportData.box_id == box_id).update({ManaboxExportData.box_id: None})
|
||||||
|
return {"status": "success", "box_id": box_id}
|
||||||
|
|
||||||
|
def update_box(self):
|
||||||
|
pass
|
||||||
|
|
136
services/product.py
Normal file
136
services/product.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from db.utils import db_transaction
|
||||||
|
from db.models import Product, File, CardManabox, Card, StagedFileProduct
|
||||||
|
from io import StringIO
|
||||||
|
import pandas as pd
|
||||||
|
from services.file import FileService
|
||||||
|
from uuid import uuid4 as uuid
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ManaboxRow:
|
||||||
|
def __init__(self, row: pd.Series):
|
||||||
|
self.name = row['name']
|
||||||
|
self.set_code = row['set_code']
|
||||||
|
self.set_name = row['set_name']
|
||||||
|
self.collector_number = row['collector_number']
|
||||||
|
self.foil = row['foil']
|
||||||
|
self.rarity = row['rarity']
|
||||||
|
self.manabox_id = row['manabox_id']
|
||||||
|
self.scryfall_id = row['scryfall_id']
|
||||||
|
self.condition = row['condition']
|
||||||
|
self.language = row['language']
|
||||||
|
self.quantity = row['quantity']
|
||||||
|
|
||||||
|
class ProductService:
|
||||||
|
def __init__(self, db: Session, file_service: FileService):
|
||||||
|
self.db = db
|
||||||
|
self.file_service = file_service
|
||||||
|
|
||||||
|
def _format_manabox_df(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
# format columns
|
||||||
|
df.columns = df.columns.str.lower()
|
||||||
|
df.columns = df.columns.str.replace(' ', '_')
|
||||||
|
return df
|
||||||
|
|
||||||
|
def _manabox_file_to_df(self, file: File) -> pd.DataFrame:
|
||||||
|
with open(file.filepath, 'rb') as f:
|
||||||
|
content = f.read()
|
||||||
|
content = content.decode('utf-8')
|
||||||
|
df = pd.read_csv(StringIO(content))
|
||||||
|
df = self._format_manabox_df(df)
|
||||||
|
return df
|
||||||
|
|
||||||
|
def create_staged_product(self, file: File, card_manabox:CardManabox, row: ManaboxRow) -> StagedFileProduct:
|
||||||
|
staged_product = StagedFileProduct(
|
||||||
|
id = str(uuid()),
|
||||||
|
file_id = file.id,
|
||||||
|
product_id = card_manabox.product_id,
|
||||||
|
quantity = row.quantity
|
||||||
|
)
|
||||||
|
with db_transaction(self.db):
|
||||||
|
self.db.add(staged_product)
|
||||||
|
return staged_product
|
||||||
|
|
||||||
|
def create_card_manabox(self, manabox_row: ManaboxRow) -> CardManabox:
|
||||||
|
card_manabox = CardManabox(
|
||||||
|
product_id = str(uuid()),
|
||||||
|
name = manabox_row.name,
|
||||||
|
set_code = manabox_row.set_code,
|
||||||
|
set_name = manabox_row.set_name,
|
||||||
|
collector_number = manabox_row.collector_number,
|
||||||
|
foil = manabox_row.foil,
|
||||||
|
rarity = manabox_row.rarity,
|
||||||
|
manabox_id = manabox_row.manabox_id,
|
||||||
|
scryfall_id = manabox_row.scryfall_id,
|
||||||
|
condition = manabox_row.condition,
|
||||||
|
language = manabox_row.language
|
||||||
|
)
|
||||||
|
return card_manabox
|
||||||
|
|
||||||
|
def create_product(self, card_manabox: CardManabox) -> Product:
|
||||||
|
product = Product(
|
||||||
|
id = card_manabox.product_id,
|
||||||
|
name = card_manabox.name,
|
||||||
|
set_code = card_manabox.set_code,
|
||||||
|
set_name = card_manabox.set_name,
|
||||||
|
type = 'card',
|
||||||
|
product_line = 'mtg'
|
||||||
|
)
|
||||||
|
return product
|
||||||
|
|
||||||
|
def create_card(self, card_manabox: CardManabox) -> Card:
|
||||||
|
card = Card(
|
||||||
|
product_id = card_manabox.product_id,
|
||||||
|
number = card_manabox.collector_number,
|
||||||
|
foil = card_manabox.foil,
|
||||||
|
rarity = card_manabox.rarity,
|
||||||
|
condition = card_manabox.condition,
|
||||||
|
language = card_manabox.language,
|
||||||
|
scryfall_id = card_manabox.scryfall_id,
|
||||||
|
manabox_id = card_manabox.manabox_id
|
||||||
|
)
|
||||||
|
return card
|
||||||
|
|
||||||
|
def card_manabox_lookup_create_if_not_exist(self, manabox_row: ManaboxRow) -> CardManabox:
|
||||||
|
# query based on all fields in manabox_row
|
||||||
|
card_manabox = self.db.query(CardManabox).filter(
|
||||||
|
CardManabox.name == manabox_row.name,
|
||||||
|
CardManabox.set_code == manabox_row.set_code,
|
||||||
|
CardManabox.set_name == manabox_row.set_name,
|
||||||
|
CardManabox.collector_number == manabox_row.collector_number,
|
||||||
|
CardManabox.foil == manabox_row.foil,
|
||||||
|
CardManabox.rarity == manabox_row.rarity,
|
||||||
|
CardManabox.manabox_id == manabox_row.manabox_id,
|
||||||
|
CardManabox.scryfall_id == manabox_row.scryfall_id,
|
||||||
|
CardManabox.condition == manabox_row.condition,
|
||||||
|
CardManabox.language == manabox_row.language
|
||||||
|
).first()
|
||||||
|
if not card_manabox:
|
||||||
|
# create new card_manabox, card, and product
|
||||||
|
with db_transaction(self.db):
|
||||||
|
card_manabox = self.create_card_manabox(manabox_row)
|
||||||
|
product = self.create_product(card_manabox)
|
||||||
|
card = self.create_card(card_manabox)
|
||||||
|
self.db.add(card_manabox)
|
||||||
|
self.db.add(product)
|
||||||
|
self.db.add(card)
|
||||||
|
return card_manabox
|
||||||
|
|
||||||
|
def bg_process_manabox_file(self, file_id: str):
|
||||||
|
try:
|
||||||
|
file = self.file_service.get_file(file_id)
|
||||||
|
df = self._manabox_file_to_df(file)
|
||||||
|
for index, row in df.iterrows():
|
||||||
|
manabox_row = ManaboxRow(row)
|
||||||
|
card_manabox = self.card_manabox_lookup_create_if_not_exist(manabox_row)
|
||||||
|
staged_product = self.create_staged_product(file, card_manabox, row)
|
||||||
|
# update file status
|
||||||
|
with db_transaction(self.db):
|
||||||
|
file.status = 'prepared'
|
||||||
|
except Exception as e:
|
||||||
|
with db_transaction(self.db):
|
||||||
|
file.status = 'error'
|
||||||
|
raise e
|
@ -41,12 +41,14 @@ class UploadService:
|
|||||||
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
def _create_file_upload_record(self, upload_id: str, filename: str) -> UploadHistory:
|
def _create_file_upload_record(self, upload_id: str, filename: str, file_size_kb: float, num_rows: int) -> UploadHistory:
|
||||||
file_upload_record = UploadHistory(
|
file_upload_record = UploadHistory(
|
||||||
id = str(uuid.uuid4()),
|
id = str(uuid.uuid4()),
|
||||||
upload_id = upload_id,
|
upload_id = upload_id,
|
||||||
filename = filename,
|
filename = filename,
|
||||||
status = "pending"
|
status = "pending",
|
||||||
|
file_size_kb = file_size_kb,
|
||||||
|
num_rows = num_rows
|
||||||
)
|
)
|
||||||
self.db.add(file_upload_record)
|
self.db.add(file_upload_record)
|
||||||
return file_upload_record
|
return file_upload_record
|
||||||
@ -80,13 +82,14 @@ class UploadService:
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def process_manabox_upload(self, content: bytes, filename: str):
|
def process_manabox_upload(self, content: bytes, filename: str, file_size_kb: float) -> dict:
|
||||||
upload = UploadObject(content=content, filename=filename)
|
upload = UploadObject(content=content, filename=filename)
|
||||||
upload.upload_id = self._create_upload_id()
|
upload.upload_id = self._create_upload_id()
|
||||||
upload.df = self._prepare_manabox_df(upload.content, upload.upload_id)
|
upload.df = self._prepare_manabox_df(upload.content, upload.upload_id)
|
||||||
|
num_rows = len(upload.df)
|
||||||
|
|
||||||
with db_transaction(self.db):
|
with db_transaction(self.db):
|
||||||
file_upload_record = self._create_file_upload_record(upload.upload_id, upload.filename)
|
file_upload_record = self._create_file_upload_record(upload.upload_id, upload.filename, file_size_kb, num_rows)
|
||||||
if not self._update_manabox_data(upload.df):
|
if not self._update_manabox_data(upload.df):
|
||||||
# set upload to failed
|
# set upload to failed
|
||||||
file_upload_record.status = "failed"
|
file_upload_record.status = "failed"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user