Initial commit - MIDAS App educação financeira para apostadores (FastAPI + Next.js)
This commit is contained in:
0
backend/app/routers/__init__.py
Normal file
0
backend/app/routers/__init__.py
Normal file
42
backend/app/routers/achievements.py
Normal file
42
backend/app/routers/achievements.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, desc
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.achievement import Achievement, UserAchievement
|
||||
from app.utils.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/achievements", tags=["achievements"])
|
||||
|
||||
@router.get("")
|
||||
async def all_achievements(db: AsyncSession = Depends(get_db)):
|
||||
achievements = (await db.execute(select(Achievement))).scalars().all()
|
||||
return [
|
||||
{"id": str(a.id), "name": a.name, "description": a.description, "icon": a.icon,
|
||||
"category": a.category, "points": a.points}
|
||||
for a in achievements
|
||||
]
|
||||
|
||||
@router.get("/mine")
|
||||
async def my_achievements(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(
|
||||
select(UserAchievement, Achievement)
|
||||
.join(Achievement, UserAchievement.achievement_id == Achievement.id)
|
||||
.where(UserAchievement.user_id == user.id)
|
||||
)
|
||||
rows = result.all()
|
||||
return [
|
||||
{"id": str(ua.id), "achievement": {"id": str(a.id), "name": a.name, "icon": a.icon, "points": a.points},
|
||||
"unlocked_at": ua.unlocked_at.isoformat()}
|
||||
for ua, a in rows
|
||||
]
|
||||
|
||||
@router.get("/leaderboard")
|
||||
async def leaderboard(db: AsyncSession = Depends(get_db)):
|
||||
users = (await db.execute(
|
||||
select(User).order_by(desc(User.total_points)).limit(20)
|
||||
)).scalars().all()
|
||||
return [
|
||||
{"name": u.name or u.email.split("@")[0], "points": u.total_points, "streak": u.streak_days}
|
||||
for u in users
|
||||
]
|
||||
28
backend/app/routers/alerts.py
Normal file
28
backend/app/routers/alerts.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.alert import Alert
|
||||
from app.utils.auth import get_current_user
|
||||
from app.services.risk_engine import calculate_risk_score
|
||||
|
||||
router = APIRouter(prefix="/api/alerts", tags=["alerts"])
|
||||
|
||||
@router.get("")
|
||||
async def get_alerts(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
risk = await calculate_risk_score(user.id, db)
|
||||
|
||||
# Get stored alerts
|
||||
alerts = (await db.execute(
|
||||
select(Alert).where(Alert.user_id == user.id).order_by(desc(Alert.created_at)).limit(20)
|
||||
)).scalars().all()
|
||||
|
||||
return {
|
||||
"current_risk": risk,
|
||||
"alerts": [
|
||||
{"id": str(a.id), "type": a.type, "severity": a.severity,
|
||||
"message": a.message, "is_read": a.is_read, "created_at": a.created_at.isoformat()}
|
||||
for a in alerts
|
||||
]
|
||||
}
|
||||
58
backend/app/routers/auth.py
Normal file
58
backend/app/routers/auth.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.bankroll import Bankroll
|
||||
from app.utils.auth import hash_password, verify_password, create_access_token, get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
email: str
|
||||
password: str
|
||||
name: str = ""
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
email: str
|
||||
password: str
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
id: str
|
||||
email: str
|
||||
name: str | None
|
||||
plan: str
|
||||
streak_days: int
|
||||
total_points: int
|
||||
risk_level: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@router.post("/register")
|
||||
async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)):
|
||||
existing = await db.execute(select(User).where(User.email == req.email))
|
||||
if existing.scalar_one_or_none():
|
||||
raise HTTPException(400, "Email already registered")
|
||||
user = User(email=req.email, password_hash=hash_password(req.password), name=req.name)
|
||||
db.add(user)
|
||||
await db.flush()
|
||||
bankroll = Bankroll(user_id=user.id, monthly_budget=500, weekly_limit=150, daily_limit=50)
|
||||
db.add(bankroll)
|
||||
await db.commit()
|
||||
token = create_access_token({"sub": str(user.id)})
|
||||
return {"access_token": token, "token_type": "bearer", "user": {"id": str(user.id), "email": user.email, "name": user.name}}
|
||||
|
||||
@router.post("/login")
|
||||
async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(User).where(User.email == req.email))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user or not verify_password(req.password, user.password_hash):
|
||||
raise HTTPException(401, "Invalid email or password")
|
||||
token = create_access_token({"sub": str(user.id)})
|
||||
return {"access_token": token, "token_type": "bearer", "user": {"id": str(user.id), "email": user.email, "name": user.name, "plan": user.plan}}
|
||||
|
||||
@router.get("/me")
|
||||
async def me(user: User = Depends(get_current_user)):
|
||||
return {"id": str(user.id), "email": user.email, "name": user.name, "plan": user.plan, "streak_days": user.streak_days, "total_points": user.total_points, "risk_level": user.risk_level}
|
||||
59
backend/app/routers/bankroll.py
Normal file
59
backend/app/routers/bankroll.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.bankroll import Bankroll
|
||||
from app.utils.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/bankroll", tags=["bankroll"])
|
||||
|
||||
class BankrollUpdate(BaseModel):
|
||||
monthly_budget: Optional[float] = None
|
||||
weekly_limit: Optional[float] = None
|
||||
daily_limit: Optional[float] = None
|
||||
bet_max_pct: Optional[float] = None
|
||||
|
||||
@router.get("")
|
||||
async def get_bankroll(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
br = (await db.execute(select(Bankroll).where(Bankroll.user_id == user.id))).scalar_one_or_none()
|
||||
if not br:
|
||||
raise HTTPException(404, "Bankroll not configured")
|
||||
return {
|
||||
"monthly_budget": float(br.monthly_budget), "weekly_limit": float(br.weekly_limit or 0),
|
||||
"daily_limit": float(br.daily_limit or 0), "bet_max_pct": float(br.bet_max_pct or 5),
|
||||
"month_spent": float(br.month_spent or 0), "week_spent": float(br.week_spent or 0),
|
||||
"day_spent": float(br.day_spent or 0), "current_balance": float(br.current_balance or 0)
|
||||
}
|
||||
|
||||
@router.put("")
|
||||
async def update_bankroll(req: BankrollUpdate, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
br = (await db.execute(select(Bankroll).where(Bankroll.user_id == user.id))).scalar_one_or_none()
|
||||
if not br:
|
||||
raise HTTPException(404, "Bankroll not configured")
|
||||
if req.monthly_budget is not None: br.monthly_budget = req.monthly_budget
|
||||
if req.weekly_limit is not None: br.weekly_limit = req.weekly_limit
|
||||
if req.daily_limit is not None: br.daily_limit = req.daily_limit
|
||||
if req.bet_max_pct is not None: br.bet_max_pct = req.bet_max_pct
|
||||
await db.commit()
|
||||
return {"status": "updated"}
|
||||
|
||||
@router.get("/check")
|
||||
async def check_bankroll(amount: float = Query(...), user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
br = (await db.execute(select(Bankroll).where(Bankroll.user_id == user.id))).scalar_one_or_none()
|
||||
if not br:
|
||||
return {"allowed": True, "warnings": []}
|
||||
warnings = []
|
||||
monthly_remaining = float(br.monthly_budget) - float(br.month_spent or 0)
|
||||
if amount > monthly_remaining:
|
||||
warnings.append(f"Excede limite mensal (restam R${monthly_remaining:.2f})")
|
||||
if br.weekly_limit and amount > (float(br.weekly_limit) - float(br.week_spent or 0)):
|
||||
warnings.append("Excede limite semanal")
|
||||
if br.daily_limit and amount > (float(br.daily_limit) - float(br.day_spent or 0)):
|
||||
warnings.append("Excede limite diário")
|
||||
max_bet = float(br.monthly_budget) * float(br.bet_max_pct or 5) / 100
|
||||
if amount > max_bet:
|
||||
warnings.append(f"Excede % máximo por aposta (max R${max_bet:.2f})")
|
||||
return {"allowed": len(warnings) == 0, "warnings": warnings}
|
||||
113
backend/app/routers/bets.py
Normal file
113
backend/app/routers/bets.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, desc
|
||||
from pydantic import BaseModel
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.bet import Bet
|
||||
from app.models.bankroll import Bankroll
|
||||
from app.utils.auth import get_current_user
|
||||
from app.services.gamification import check_achievements
|
||||
|
||||
router = APIRouter(prefix="/api/bets", tags=["bets"])
|
||||
|
||||
class BetCreate(BaseModel):
|
||||
sport: str
|
||||
event_name: str
|
||||
platform: str = "Bet365"
|
||||
amount: float
|
||||
odds: float
|
||||
bet_type: str = ""
|
||||
emotion: str = ""
|
||||
is_impulsive: bool = False
|
||||
|
||||
class BetResult(BaseModel):
|
||||
result: str # win, loss, void
|
||||
|
||||
@router.post("")
|
||||
async def create_bet(req: BetCreate, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
bet = Bet(
|
||||
user_id=user.id, sport=req.sport, event_name=req.event_name,
|
||||
platform=req.platform, amount=req.amount, odds=req.odds,
|
||||
bet_type=req.bet_type, emotion=req.emotion, is_impulsive=req.is_impulsive,
|
||||
result="pending"
|
||||
)
|
||||
db.add(bet)
|
||||
# Update bankroll spending
|
||||
br = (await db.execute(select(Bankroll).where(Bankroll.user_id == user.id))).scalar_one_or_none()
|
||||
if br:
|
||||
br.month_spent = float(br.month_spent or 0) + req.amount
|
||||
br.week_spent = float(br.week_spent or 0) + req.amount
|
||||
br.day_spent = float(br.day_spent or 0) + req.amount
|
||||
await db.commit()
|
||||
await db.refresh(bet)
|
||||
await check_achievements(user.id, db)
|
||||
return {"id": str(bet.id), "status": "created"}
|
||||
|
||||
@router.get("")
|
||||
async def list_bets(
|
||||
sport: Optional[str] = None,
|
||||
result: Optional[str] = None,
|
||||
limit: int = Query(50, le=200),
|
||||
offset: int = 0,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
q = select(Bet).where(Bet.user_id == user.id)
|
||||
if sport:
|
||||
q = q.where(Bet.sport == sport)
|
||||
if result:
|
||||
q = q.where(Bet.result == result)
|
||||
q = q.order_by(desc(Bet.created_at)).offset(offset).limit(limit)
|
||||
rows = (await db.execute(q)).scalars().all()
|
||||
return [
|
||||
{"id": str(b.id), "sport": b.sport, "event_name": b.event_name, "platform": b.platform,
|
||||
"amount": float(b.amount), "odds": float(b.odds) if b.odds else None, "result": b.result,
|
||||
"profit": float(b.profit) if b.profit else 0, "emotion": b.emotion,
|
||||
"is_impulsive": b.is_impulsive, "created_at": b.created_at.isoformat()}
|
||||
for b in rows
|
||||
]
|
||||
|
||||
@router.patch("/{bet_id}/result")
|
||||
async def update_result(bet_id: str, req: BetResult, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
bet = (await db.execute(select(Bet).where(Bet.id == bet_id, Bet.user_id == user.id))).scalar_one_or_none()
|
||||
if not bet:
|
||||
raise HTTPException(404, "Bet not found")
|
||||
bet.result = req.result
|
||||
if req.result == "win":
|
||||
bet.profit = float(bet.amount) * (float(bet.odds) - 1)
|
||||
elif req.result == "loss":
|
||||
bet.profit = -float(bet.amount)
|
||||
else:
|
||||
bet.profit = 0
|
||||
await db.commit()
|
||||
return {"id": str(bet.id), "result": bet.result, "profit": float(bet.profit)}
|
||||
|
||||
@router.get("/stats")
|
||||
async def bet_stats(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
bets = (await db.execute(select(Bet).where(Bet.user_id == user.id))).scalars().all()
|
||||
total = len(bets)
|
||||
wins = sum(1 for b in bets if b.result == "win")
|
||||
losses = sum(1 for b in bets if b.result == "loss")
|
||||
pending = sum(1 for b in bets if b.result == "pending")
|
||||
total_profit = sum(float(b.profit or 0) for b in bets)
|
||||
total_staked = sum(float(b.amount) for b in bets)
|
||||
win_rate = (wins / (wins + losses) * 100) if (wins + losses) > 0 else 0
|
||||
roi = (total_profit / total_staked * 100) if total_staked > 0 else 0
|
||||
|
||||
by_sport = {}
|
||||
for b in bets:
|
||||
s = b.sport or "other"
|
||||
if s not in by_sport:
|
||||
by_sport[s] = {"count": 0, "profit": 0}
|
||||
by_sport[s]["count"] += 1
|
||||
by_sport[s]["profit"] += float(b.profit or 0)
|
||||
|
||||
return {
|
||||
"total": total, "wins": wins, "losses": losses, "pending": pending,
|
||||
"total_profit": round(total_profit, 2), "total_staked": round(total_staked, 2),
|
||||
"win_rate": round(win_rate, 1), "roi": round(roi, 1), "by_sport": by_sport
|
||||
}
|
||||
53
backend/app/routers/dashboard.py
Normal file
53
backend/app/routers/dashboard.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.bet import Bet
|
||||
from app.models.bankroll import Bankroll
|
||||
from app.utils.auth import get_current_user
|
||||
from app.services.risk_engine import calculate_risk_score
|
||||
|
||||
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
|
||||
|
||||
@router.get("")
|
||||
async def get_dashboard(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
# Risk
|
||||
risk = await calculate_risk_score(user.id, db)
|
||||
|
||||
# Bankroll
|
||||
br = (await db.execute(select(Bankroll).where(Bankroll.user_id == user.id))).scalar_one_or_none()
|
||||
bankroll_data = None
|
||||
if br:
|
||||
bankroll_data = {
|
||||
"monthly_budget": float(br.monthly_budget),
|
||||
"month_spent": float(br.month_spent or 0),
|
||||
"pct_used": round(float(br.month_spent or 0) / float(br.monthly_budget) * 100, 1) if float(br.monthly_budget) > 0 else 0
|
||||
}
|
||||
|
||||
# Recent bets
|
||||
recent = (await db.execute(
|
||||
select(Bet).where(Bet.user_id == user.id).order_by(desc(Bet.created_at)).limit(5)
|
||||
)).scalars().all()
|
||||
bets_data = [
|
||||
{"id": str(b.id), "sport": b.sport, "event_name": b.event_name, "amount": float(b.amount),
|
||||
"odds": float(b.odds) if b.odds else None, "result": b.result, "profit": float(b.profit or 0),
|
||||
"created_at": b.created_at.isoformat()}
|
||||
for b in recent
|
||||
]
|
||||
|
||||
# Insights
|
||||
insights = [
|
||||
{"icon": "📊", "title": "Análise de padrões", "text": "Suas apostas em futebol têm 15% mais ROI que em outros esportes."},
|
||||
{"icon": "⏰", "title": "Horário ideal", "text": "Apostas feitas antes das 18h têm taxa de acerto 23% maior."},
|
||||
{"icon": "🧠", "title": "Controle emocional", "text": "Quando aposta com emoção 😎, seu win rate é 20% superior."},
|
||||
]
|
||||
|
||||
return {
|
||||
"risk": risk,
|
||||
"bankroll": bankroll_data,
|
||||
"streak_days": user.streak_days,
|
||||
"total_points": user.total_points,
|
||||
"recent_bets": bets_data,
|
||||
"insights": insights
|
||||
}
|
||||
41
backend/app/routers/lessons.py
Normal file
41
backend/app/routers/lessons.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.lesson import Lesson, UserLesson
|
||||
from app.utils.auth import get_current_user
|
||||
from app.services.gamification import check_achievements
|
||||
|
||||
router = APIRouter(prefix="/api/lessons", tags=["lessons"])
|
||||
|
||||
@router.get("")
|
||||
async def list_lessons(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
lessons = (await db.execute(select(Lesson).order_by(Lesson.order_num))).scalars().all()
|
||||
completed = (await db.execute(
|
||||
select(UserLesson.lesson_id).where(UserLesson.user_id == user.id)
|
||||
)).scalars().all()
|
||||
completed_ids = set(completed)
|
||||
return [
|
||||
{"id": str(l.id), "title": l.title, "category": l.category, "difficulty": l.difficulty,
|
||||
"duration_min": l.duration_min, "content": l.content, "is_premium": l.is_premium,
|
||||
"completed": l.id in completed_ids}
|
||||
for l in lessons
|
||||
]
|
||||
|
||||
@router.post("/{lesson_id}/complete")
|
||||
async def complete_lesson(lesson_id: str, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
lesson = (await db.execute(select(Lesson).where(Lesson.id == lesson_id))).scalar_one_or_none()
|
||||
if not lesson:
|
||||
raise HTTPException(404, "Lesson not found")
|
||||
existing = (await db.execute(
|
||||
select(UserLesson).where(UserLesson.user_id == user.id, UserLesson.lesson_id == lesson_id)
|
||||
)).scalar_one_or_none()
|
||||
if existing:
|
||||
return {"status": "already_completed"}
|
||||
ul = UserLesson(user_id=user.id, lesson_id=lesson_id)
|
||||
db.add(ul)
|
||||
user.total_points = (user.total_points or 0) + 10
|
||||
await db.commit()
|
||||
await check_achievements(user.id, db)
|
||||
return {"status": "completed", "points_earned": 10}
|
||||
60
backend/app/routers/reports.py
Normal file
60
backend/app/routers/reports.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from datetime import datetime, timedelta
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.bet import Bet
|
||||
from app.utils.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/reports", tags=["reports"])
|
||||
|
||||
def _build_report(bets):
|
||||
total = len(bets)
|
||||
wins = sum(1 for b in bets if b.result == "win")
|
||||
losses = sum(1 for b in bets if b.result == "loss")
|
||||
profit = sum(float(b.profit or 0) for b in bets)
|
||||
staked = sum(float(b.amount) for b in bets)
|
||||
|
||||
by_sport = {}
|
||||
by_hour = {}
|
||||
daily_pl = {}
|
||||
for b in bets:
|
||||
s = b.sport or "other"
|
||||
by_sport.setdefault(s, {"count": 0, "profit": 0})
|
||||
by_sport[s]["count"] += 1
|
||||
by_sport[s]["profit"] += float(b.profit or 0)
|
||||
|
||||
h = b.created_at.hour
|
||||
by_hour.setdefault(h, {"count": 0, "profit": 0})
|
||||
by_hour[h]["count"] += 1
|
||||
by_hour[h]["profit"] += float(b.profit or 0)
|
||||
|
||||
day = b.created_at.strftime("%Y-%m-%d")
|
||||
daily_pl.setdefault(day, 0)
|
||||
daily_pl[day] += float(b.profit or 0)
|
||||
|
||||
return {
|
||||
"total_bets": total, "wins": wins, "losses": losses,
|
||||
"total_profit": round(profit, 2), "total_staked": round(staked, 2),
|
||||
"win_rate": round(wins / (wins + losses) * 100, 1) if (wins + losses) > 0 else 0,
|
||||
"roi": round(profit / staked * 100, 1) if staked > 0 else 0,
|
||||
"by_sport": by_sport, "by_hour": {str(k): v for k, v in sorted(by_hour.items())},
|
||||
"daily_pl": daily_pl
|
||||
}
|
||||
|
||||
@router.get("/weekly")
|
||||
async def weekly_report(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
since = datetime.utcnow() - timedelta(days=7)
|
||||
bets = (await db.execute(
|
||||
select(Bet).where(Bet.user_id == user.id, Bet.created_at >= since)
|
||||
)).scalars().all()
|
||||
return _build_report(bets)
|
||||
|
||||
@router.get("/monthly")
|
||||
async def monthly_report(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
since = datetime.utcnow() - timedelta(days=30)
|
||||
bets = (await db.execute(
|
||||
select(Bet).where(Bet.user_id == user.id, Bet.created_at >= since)
|
||||
)).scalars().all()
|
||||
return _build_report(bets)
|
||||
Reference in New Issue
Block a user