This commit is contained in:
2026-01-05 07:15:50 +07:00
parent 65b2512d8c
commit 6a7717a474
44 changed files with 5678 additions and 183 deletions

View File

@@ -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",
]

View File

@@ -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):

View File

@@ -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
View 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

View File

@@ -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