feat: implement chest opening core logic with unique checks and audit

This commit is contained in:
2026-02-09 14:45:25 +01:00
parent 01f5c33042
commit 26c629d605
3 changed files with 156 additions and 1 deletions

View File

@@ -3,7 +3,7 @@ from sqlalchemy.orm import Session
from sqlalchemy import text
from app.database import get_db, engine
from app import models, seed
from app.routers import auth, users
from app.routers import auth, users, chests
# Create all tables
models.Base.metadata.create_all(bind=engine)
@@ -12,6 +12,7 @@ app = FastAPI(title="Card Game Backend")
app.include_router(auth.router)
app.include_router(users.router)
app.include_router(chests.router)
@app.on_event("startup")
def startup_event():

View File

@@ -0,0 +1,141 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app import models, dependencies, schemas
from app.database import get_db
import random
router = APIRouter(prefix="/chests", tags=["chests"])
DUPLICATE_CONVERSION_GOLD = 20
RARITY_WEIGHTS = {
"COMMON": 70,
"RARE": 25,
"LEGENDARY": 5
}
def roll_rarity():
rand = random.randint(1, 100)
if rand <= 70:
return "COMMON"
elif rand <= 95:
return "RARE"
else:
return "LEGENDARY"
@router.post("/{chest_id}/open", response_model=schemas.ChestOpenResponse)
def open_chest(chest_id: str, current_user: models.User = Depends(dependencies.get_current_user), db: Session = Depends(get_db)):
# 1. Fetch Chest
chest = db.query(models.Chest).filter(models.Chest.id == chest_id).first()
if not chest:
raise HTTPException(status_code=404, detail="Chest not found")
# 2. Check Balance
if not current_user.wallet or current_user.wallet.balance < chest.cost_gold:
raise HTTPException(status_code=403, detail="Insufficient gold")
# 3. Start Logic (Implicit transaction via Session)
try:
# Debit Wallet
current_user.wallet.balance -= chest.cost_gold
db.add(current_user.wallet)
# Log Debit Transaction
debit_tx = models.WalletTransaction(
user_id=current_user.id,
type="DEBIT",
amount=chest.cost_gold,
reason="CHEST_OPEN"
)
db.add(debit_tx)
# Flush to get IDs if needed, but not strictly necessary yet.
# Using flush helps catch DB errors early.
db.flush()
# Create ChestOpen Audit
chest_open = models.ChestOpen(
user_id=current_user.id,
chest_id=chest.id,
spent_gold=chest.cost_gold
)
db.add(chest_open)
db.flush() # Get chest_open.id
items_response = []
# Track cards obtained in this transaction to prevent valid duplicates within the same chest
cards_obtained_ids = set()
# 4. Roll Cards
for _ in range(chest.cards_per_open):
rarity = roll_rarity()
# Fetch all cards of this rarity
potential_cards = db.query(models.Card).filter(models.Card.rarity == rarity).all()
if not potential_cards:
# Should not happen in seeded DB, but handle safely
outcome = "CONVERTED"
card = None
converted_gold = DUPLICATE_CONVERSION_GOLD
else:
card = random.choice(potential_cards)
# Check if user owns it (DB check + local transaction check)
user_owned = db.query(models.UserCard).filter(
models.UserCard.user_id == current_user.id,
models.UserCard.card_id == card.id
).first()
if user_owned or card.id in cards_obtained_ids:
outcome = "CONVERTED"
converted_gold = DUPLICATE_CONVERSION_GOLD
else:
outcome = "NEW"
converted_gold = None
# Add to collection
new_user_card = models.UserCard(user_id=current_user.id, card_id=card.id)
db.add(new_user_card)
cards_obtained_ids.add(card.id)
# Handle Conversion Credit
if outcome == "CONVERTED":
current_user.wallet.balance += converted_gold
credit_tx = models.WalletTransaction(
user_id=current_user.id,
type="CREDIT",
amount=converted_gold,
reason="DUPLICATE_CONVERT",
reference_id=chest_open.id
)
db.add(credit_tx)
# Log Item
open_item = models.ChestOpenItem(
chest_open_id=chest_open.id,
card_id=card.id if card else None,
rarity=rarity,
outcome_type=outcome,
converted_gold=converted_gold
)
db.add(open_item)
items_response.append({
"card_name": card.name if card else None,
"rarity": rarity,
"outcome": outcome,
"converted_gold": converted_gold
})
db.commit()
db.refresh(current_user.wallet)
return {
"chest_open_id": chest_open.id,
"spent_gold": chest.cost_gold,
"items": items_response
}
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=f"Transaction failed: {str(e)}")

View File

@@ -20,3 +20,16 @@ class UserResponse(BaseModel):
class Config:
orm_mode = True
from typing import List, Optional
class ChestOpenItemResponse(BaseModel):
card_name: Optional[str] = None
rarity: str
outcome: str # NEW / CONVERTED
converted_gold: Optional[int] = None
class ChestOpenResponse(BaseModel):
chest_open_id: str
spent_gold: int
items: List[ChestOpenItemResponse]