Add shop
This commit is contained in:
@@ -104,6 +104,27 @@ from app.schemas.admin import (
|
||||
LoginResponse,
|
||||
DashboardStats,
|
||||
)
|
||||
from app.schemas.shop import (
|
||||
ShopItemCreate,
|
||||
ShopItemUpdate,
|
||||
ShopItemResponse,
|
||||
InventoryItemResponse,
|
||||
PurchaseRequest,
|
||||
PurchaseResponse,
|
||||
UseConsumableRequest,
|
||||
UseConsumableResponse,
|
||||
EquipItemRequest,
|
||||
EquipItemResponse,
|
||||
CoinTransactionResponse,
|
||||
CoinsBalanceResponse,
|
||||
AdminCoinsRequest,
|
||||
UserCosmeticsResponse,
|
||||
CertificationRequestSchema,
|
||||
CertificationReviewRequest,
|
||||
CertificationStatusResponse,
|
||||
ConsumablesStatusResponse,
|
||||
)
|
||||
from app.schemas.user import ShopItemPublic
|
||||
|
||||
__all__ = [
|
||||
# User
|
||||
@@ -202,4 +223,24 @@ __all__ = [
|
||||
"TwoFactorVerifyRequest",
|
||||
"LoginResponse",
|
||||
"DashboardStats",
|
||||
# Shop
|
||||
"ShopItemCreate",
|
||||
"ShopItemUpdate",
|
||||
"ShopItemResponse",
|
||||
"ShopItemPublic",
|
||||
"InventoryItemResponse",
|
||||
"PurchaseRequest",
|
||||
"PurchaseResponse",
|
||||
"UseConsumableRequest",
|
||||
"UseConsumableResponse",
|
||||
"EquipItemRequest",
|
||||
"EquipItemResponse",
|
||||
"CoinTransactionResponse",
|
||||
"CoinsBalanceResponse",
|
||||
"AdminCoinsRequest",
|
||||
"UserCosmeticsResponse",
|
||||
"CertificationRequestSchema",
|
||||
"CertificationReviewRequest",
|
||||
"CertificationStatusResponse",
|
||||
"ConsumablesStatusResponse",
|
||||
]
|
||||
|
||||
@@ -77,12 +77,14 @@ class CompleteResult(BaseModel):
|
||||
streak_bonus: int
|
||||
total_points: int
|
||||
new_streak: int
|
||||
coins_earned: int = 0 # Coins earned (only in certified marathons)
|
||||
|
||||
|
||||
class DropResult(BaseModel):
|
||||
penalty: int
|
||||
total_points: int
|
||||
new_drop_count: int
|
||||
shield_used: bool = False # Whether shield consumable was used to prevent penalty
|
||||
|
||||
|
||||
class EventAssignmentResponse(BaseModel):
|
||||
|
||||
@@ -14,6 +14,10 @@ class MarathonCreate(MarathonBase):
|
||||
duration_days: int = Field(default=30, ge=1, le=365)
|
||||
is_public: bool = False
|
||||
game_proposal_mode: str = Field(default="all_participants", pattern="^(all_participants|organizer_only)$")
|
||||
# Shop/Consumables settings
|
||||
allow_skips: bool = True
|
||||
max_skips_per_participant: int | None = Field(None, ge=1, le=100)
|
||||
allow_consumables: bool = True
|
||||
|
||||
|
||||
class MarathonUpdate(BaseModel):
|
||||
@@ -23,6 +27,10 @@ class MarathonUpdate(BaseModel):
|
||||
is_public: bool | None = None
|
||||
game_proposal_mode: str | None = Field(None, pattern="^(all_participants|organizer_only)$")
|
||||
auto_events_enabled: bool | None = None
|
||||
# Shop/Consumables settings
|
||||
allow_skips: bool | None = None
|
||||
max_skips_per_participant: int | None = Field(None, ge=1, le=100)
|
||||
allow_consumables: bool | None = None
|
||||
|
||||
|
||||
class ParticipantInfo(BaseModel):
|
||||
@@ -32,6 +40,13 @@ class ParticipantInfo(BaseModel):
|
||||
current_streak: int
|
||||
drop_count: int
|
||||
joined_at: datetime
|
||||
# Shop: coins and consumables status
|
||||
coins_earned: int = 0
|
||||
skips_used: int = 0
|
||||
has_shield: bool = False
|
||||
has_active_boost: bool = False
|
||||
boost_multiplier: float | None = None
|
||||
boost_expires_at: datetime | None = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -56,6 +71,13 @@ class MarathonResponse(MarathonBase):
|
||||
games_count: int
|
||||
created_at: datetime
|
||||
my_participation: ParticipantInfo | None = None
|
||||
# Certification
|
||||
certification_status: str = "none"
|
||||
is_certified: bool = False
|
||||
# Shop/Consumables settings
|
||||
allow_skips: bool = True
|
||||
max_skips_per_participant: int | None = None
|
||||
allow_consumables: bool = True
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -74,6 +96,8 @@ class MarathonListItem(BaseModel):
|
||||
participants_count: int
|
||||
start_date: datetime | None
|
||||
end_date: datetime | None
|
||||
# Certification badge
|
||||
is_certified: bool = False
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
199
backend/app/schemas/shop.py
Normal file
199
backend/app/schemas/shop.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
Pydantic schemas for Shop system
|
||||
"""
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Any
|
||||
|
||||
|
||||
# === Shop Items ===
|
||||
|
||||
class ShopItemBase(BaseModel):
|
||||
"""Base schema for shop items"""
|
||||
item_type: str
|
||||
code: str
|
||||
name: str
|
||||
description: str | None = None
|
||||
price: int
|
||||
rarity: str = "common"
|
||||
asset_data: dict | None = None
|
||||
|
||||
|
||||
class ShopItemCreate(ShopItemBase):
|
||||
"""Schema for creating a shop item (admin)"""
|
||||
is_active: bool = True
|
||||
available_from: datetime | None = None
|
||||
available_until: datetime | None = None
|
||||
stock_limit: int | None = None
|
||||
|
||||
|
||||
class ShopItemUpdate(BaseModel):
|
||||
"""Schema for updating a shop item (admin)"""
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
price: int | None = Field(None, ge=1)
|
||||
rarity: str | None = None
|
||||
asset_data: dict | None = None
|
||||
is_active: bool | None = None
|
||||
available_from: datetime | None = None
|
||||
available_until: datetime | None = None
|
||||
stock_limit: int | None = None
|
||||
|
||||
|
||||
class ShopItemResponse(ShopItemBase):
|
||||
"""Schema for shop item response"""
|
||||
id: int
|
||||
is_active: bool
|
||||
available_from: datetime | None
|
||||
available_until: datetime | None
|
||||
stock_limit: int | None
|
||||
stock_remaining: int | None
|
||||
created_at: datetime
|
||||
is_available: bool # Computed property
|
||||
is_owned: bool = False # Set by API based on user
|
||||
is_equipped: bool = False # Set by API based on user
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# === Inventory ===
|
||||
|
||||
class InventoryItemResponse(BaseModel):
|
||||
"""Schema for user inventory item"""
|
||||
id: int
|
||||
item: ShopItemResponse
|
||||
quantity: int
|
||||
equipped: bool
|
||||
purchased_at: datetime
|
||||
expires_at: datetime | None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# === Purchases ===
|
||||
|
||||
class PurchaseRequest(BaseModel):
|
||||
"""Schema for purchase request"""
|
||||
item_id: int
|
||||
quantity: int = Field(default=1, ge=1, le=10)
|
||||
|
||||
|
||||
class PurchaseResponse(BaseModel):
|
||||
"""Schema for purchase response"""
|
||||
success: bool
|
||||
item: ShopItemResponse
|
||||
quantity: int
|
||||
total_cost: int
|
||||
new_balance: int
|
||||
message: str
|
||||
|
||||
|
||||
# === Consumables ===
|
||||
|
||||
class UseConsumableRequest(BaseModel):
|
||||
"""Schema for using a consumable"""
|
||||
item_code: str # 'skip', 'shield', 'boost', 'reroll'
|
||||
marathon_id: int
|
||||
assignment_id: int | None = None # Required for skip and reroll
|
||||
|
||||
|
||||
class UseConsumableResponse(BaseModel):
|
||||
"""Schema for consumable use response"""
|
||||
success: bool
|
||||
item_code: str
|
||||
remaining_quantity: int
|
||||
effect_description: str
|
||||
effect_data: dict | None = None
|
||||
|
||||
|
||||
# === Equipment ===
|
||||
|
||||
class EquipItemRequest(BaseModel):
|
||||
"""Schema for equipping an item"""
|
||||
inventory_id: int
|
||||
|
||||
|
||||
class EquipItemResponse(BaseModel):
|
||||
"""Schema for equip response"""
|
||||
success: bool
|
||||
item_type: str
|
||||
equipped_item: ShopItemResponse | None
|
||||
message: str
|
||||
|
||||
|
||||
# === Coins ===
|
||||
|
||||
class CoinTransactionResponse(BaseModel):
|
||||
"""Schema for coin transaction"""
|
||||
id: int
|
||||
amount: int
|
||||
transaction_type: str
|
||||
description: str | None
|
||||
reference_type: str | None
|
||||
reference_id: int | None
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class CoinsBalanceResponse(BaseModel):
|
||||
"""Schema for coins balance with recent transactions"""
|
||||
balance: int
|
||||
recent_transactions: list[CoinTransactionResponse]
|
||||
|
||||
|
||||
class AdminCoinsRequest(BaseModel):
|
||||
"""Schema for admin coin operations"""
|
||||
amount: int = Field(..., ge=1)
|
||||
reason: str = Field(..., min_length=1, max_length=500)
|
||||
|
||||
|
||||
# === User Cosmetics ===
|
||||
|
||||
class UserCosmeticsResponse(BaseModel):
|
||||
"""Schema for user's equipped cosmetics"""
|
||||
frame: ShopItemResponse | None = None
|
||||
title: ShopItemResponse | None = None
|
||||
name_color: ShopItemResponse | None = None
|
||||
background: ShopItemResponse | None = None
|
||||
|
||||
|
||||
# === Certification ===
|
||||
|
||||
class CertificationRequestSchema(BaseModel):
|
||||
"""Schema for requesting marathon certification"""
|
||||
pass # No fields needed for now
|
||||
|
||||
|
||||
class CertificationReviewRequest(BaseModel):
|
||||
"""Schema for admin reviewing certification"""
|
||||
approve: bool
|
||||
rejection_reason: str | None = Field(None, max_length=1000)
|
||||
|
||||
|
||||
class CertificationStatusResponse(BaseModel):
|
||||
"""Schema for certification status"""
|
||||
marathon_id: int
|
||||
certification_status: str
|
||||
is_certified: bool
|
||||
certification_requested_at: datetime | None
|
||||
certified_at: datetime | None
|
||||
certified_by_nickname: str | None = None
|
||||
rejection_reason: str | None = None
|
||||
|
||||
|
||||
# === Consumables Status ===
|
||||
|
||||
class ConsumablesStatusResponse(BaseModel):
|
||||
"""Schema for participant's consumables status in a marathon"""
|
||||
skips_available: int # From inventory
|
||||
skips_used: int # In this marathon
|
||||
skips_remaining: int | None # Based on marathon limit
|
||||
has_shield: bool
|
||||
has_active_boost: bool
|
||||
boost_multiplier: float | None
|
||||
boost_expires_at: datetime | None
|
||||
rerolls_available: int # From inventory
|
||||
@@ -28,6 +28,19 @@ class UserUpdate(BaseModel):
|
||||
nickname: str | None = Field(None, min_length=2, max_length=50)
|
||||
|
||||
|
||||
class ShopItemPublic(BaseModel):
|
||||
"""Minimal shop item info for public display"""
|
||||
id: int
|
||||
code: str
|
||||
name: str
|
||||
item_type: str
|
||||
rarity: str
|
||||
asset_data: dict | None = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class UserPublic(UserBase):
|
||||
"""Public user info visible to other users - minimal data"""
|
||||
id: int
|
||||
@@ -35,6 +48,11 @@ class UserPublic(UserBase):
|
||||
role: str = "user"
|
||||
telegram_avatar_url: str | None = None # Only TG avatar is public
|
||||
created_at: datetime
|
||||
# Shop: equipped cosmetics (visible to others)
|
||||
equipped_frame: ShopItemPublic | None = None
|
||||
equipped_title: ShopItemPublic | None = None
|
||||
equipped_name_color: ShopItemPublic | None = None
|
||||
equipped_background: ShopItemPublic | None = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -51,6 +69,8 @@ class UserPrivate(UserPublic):
|
||||
notify_events: bool = True
|
||||
notify_disputes: bool = True
|
||||
notify_moderation: bool = True
|
||||
# Shop: coins balance (only visible to self)
|
||||
coins_balance: int = 0
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
@@ -82,8 +102,15 @@ class UserProfilePublic(BaseModel):
|
||||
id: int
|
||||
nickname: str
|
||||
avatar_url: str | None = None
|
||||
telegram_avatar_url: str | None = None
|
||||
role: str = "user"
|
||||
created_at: datetime
|
||||
stats: UserStats
|
||||
# Equipped cosmetics
|
||||
equipped_frame: ShopItemPublic | None = None
|
||||
equipped_title: ShopItemPublic | None = None
|
||||
equipped_name_color: ShopItemPublic | None = None
|
||||
equipped_background: ShopItemPublic | None = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
Reference in New Issue
Block a user