Initial commit - MIDAS App educação financeira para apostadores (FastAPI + Next.js)

This commit is contained in:
bigtux
2026-02-10 18:52:23 -03:00
commit 954ebccdd6
31 changed files with 1701 additions and 0 deletions

0
backend/app/__init__.py Normal file
View File

12
backend/app/config.py Normal file
View File

@@ -0,0 +1,12 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str = "postgresql+asyncpg://midas:Midas2026!@localhost/midas"
SECRET_KEY: str = "midas-secret-key-change-in-production-2026"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440
class Config:
env_file = ".env"
settings = Settings()

17
backend/app/database.py Normal file
View File

@@ -0,0 +1,17 @@
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import DeclarativeBase
from app.config import settings
engine = create_async_engine(settings.DATABASE_URL, echo=False)
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
class Base(DeclarativeBase):
pass
async def get_db():
async with async_session() as session:
yield session
async def init_db():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)

33
backend/app/main.py Normal file
View File

@@ -0,0 +1,33 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.database import init_db
from app.routers import auth, bets, bankroll, dashboard, alerts, achievements, lessons, reports
@asynccontextmanager
async def lifespan(app: FastAPI):
await init_db()
yield
app = FastAPI(title="MIDAS API", version="1.0.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3085", "http://magneto:3085", "*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(auth.router)
app.include_router(bets.router)
app.include_router(bankroll.router)
app.include_router(dashboard.router)
app.include_router(alerts.router)
app.include_router(achievements.router)
app.include_router(lessons.router)
app.include_router(reports.router)
@app.get("/api/health")
async def health():
return {"status": "ok", "app": "MIDAS"}

View File

View File

@@ -0,0 +1,27 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, String, Integer, DateTime, Text, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database import Base
class Achievement(Base):
__tablename__ = "achievements"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String(100), nullable=False)
description = Column(Text)
icon = Column(String(10))
category = Column(String(50))
requirement_type = Column(String(50))
requirement_value = Column(Integer)
points = Column(Integer, default=10)
class UserAchievement(Base):
__tablename__ = "user_achievements"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
achievement_id = Column(UUID(as_uuid=True), ForeignKey("achievements.id"), nullable=False)
unlocked_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="achievements")
achievement = relationship("Achievement")

View File

@@ -0,0 +1,19 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, String, DateTime, Text, Boolean, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database import Base
class Alert(Base):
__tablename__ = "alerts"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
type = Column(String(50), nullable=False)
severity = Column(String(10), nullable=False) # green, yellow, red
message = Column(Text, nullable=False)
ai_analysis = Column(Text)
is_read = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="alerts")

View File

@@ -0,0 +1,23 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, String, Integer, DateTime, Numeric, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database import Base
class Bankroll(Base):
__tablename__ = "bankrolls"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), unique=True, nullable=False)
monthly_budget = Column(Numeric(10, 2), nullable=False, default=500)
weekly_limit = Column(Numeric(10, 2), default=150)
daily_limit = Column(Numeric(10, 2), default=50)
bet_max_pct = Column(Numeric(5, 2), default=5.0)
current_balance = Column(Numeric(10, 2), default=0)
month_spent = Column(Numeric(10, 2), default=0)
week_spent = Column(Numeric(10, 2), default=0)
day_spent = Column(Numeric(10, 2), default=0)
reset_day = Column(Integer, default=1)
created_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="bankroll")

24
backend/app/models/bet.py Normal file
View File

@@ -0,0 +1,24 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, String, Integer, DateTime, Numeric, Boolean, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database import Base
class Bet(Base):
__tablename__ = "bets"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
sport = Column(String(50))
event_name = Column(String(255))
platform = Column(String(100))
amount = Column(Numeric(10, 2), nullable=False)
odds = Column(Numeric(8, 3))
result = Column(String(10)) # win, loss, pending
profit = Column(Numeric(10, 2), default=0)
bet_type = Column(String(50))
is_impulsive = Column(Boolean, default=False)
emotion = Column(String(50))
created_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="bets")

View File

@@ -0,0 +1,27 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, String, Integer, DateTime, Text, Boolean, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database import Base
class Lesson(Base):
__tablename__ = "lessons"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
title = Column(String(255), nullable=False)
content = Column(Text, nullable=False)
category = Column(String(50))
difficulty = Column(String(20))
duration_min = Column(Integer, default=5)
order_num = Column(Integer)
is_premium = Column(Boolean, default=False)
class UserLesson(Base):
__tablename__ = "user_lessons"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
lesson_id = Column(UUID(as_uuid=True), ForeignKey("lessons.id"), nullable=False)
completed_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="completed_lessons")
lesson = relationship("Lesson")

View File

@@ -0,0 +1,24 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, String, Integer, DateTime, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database import Base
class User(Base):
__tablename__ = "users"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
email = Column(String(255), unique=True, nullable=False, index=True)
password_hash = Column(String(255), nullable=False)
name = Column(String(100))
plan = Column(String(20), default="free")
streak_days = Column(Integer, default=0)
total_points = Column(Integer, default=0)
risk_level = Column(String(10), default="green")
created_at = Column(DateTime, default=datetime.utcnow)
bets = relationship("Bet", back_populates="user")
bankroll = relationship("Bankroll", back_populates="user", uselist=False)
achievements = relationship("UserAchievement", back_populates="user")
completed_lessons = relationship("UserLesson", back_populates="user")
alerts = relationship("Alert", back_populates="user")

View File

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

View 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
]
}

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

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

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

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

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

View File

View File

@@ -0,0 +1,40 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.models.achievement import Achievement, UserAchievement
from app.models.bet import Bet
from app.models.lesson import UserLesson
async def check_achievements(user_id, db: AsyncSession):
"""Check and award new achievements"""
achievements = (await db.execute(select(Achievement))).scalars().all()
existing = (await db.execute(
select(UserAchievement.achievement_id).where(UserAchievement.user_id == user_id)
)).scalars().all()
existing_ids = set(existing)
bet_count = (await db.execute(
select(func.count()).select_from(Bet).where(Bet.user_id == user_id)
)).scalar() or 0
lesson_count = (await db.execute(
select(func.count()).select_from(UserLesson).where(UserLesson.user_id == user_id)
)).scalar() or 0
new_badges = []
for a in achievements:
if a.id in existing_ids:
continue
awarded = False
if a.requirement_type == "bets" and bet_count >= (a.requirement_value or 1):
awarded = True
elif a.requirement_type == "lessons" and lesson_count >= (a.requirement_value or 1):
awarded = True
if awarded:
ua = UserAchievement(user_id=user_id, achievement_id=a.id)
db.add(ua)
new_badges.append(a.name)
if new_badges:
await db.commit()
return new_badges

View File

@@ -0,0 +1,79 @@
from datetime import datetime, timedelta
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.models.bet import Bet
from app.models.bankroll import Bankroll
async def calculate_risk_score(user_id, db: AsyncSession) -> dict:
score = 0
factors = []
now = datetime.utcnow()
# 1. Horário noturno (22h-5h) +15
hour = now.hour
if hour >= 22 or hour < 5:
score += 15
factors.append("Apostando em horário noturno")
# 2. Valor crescente +25
recent = await db.execute(
select(Bet).where(Bet.user_id == user_id).order_by(Bet.created_at.desc()).limit(5)
)
recent_bets = recent.scalars().all()
if len(recent_bets) >= 3:
amounts = [float(b.amount) for b in recent_bets[:3]]
if amounts[0] > amounts[1] > amounts[2]:
score += 25
factors.append("Valores de aposta crescentes")
# 3. Recuperação (aposta logo após loss) +30
if len(recent_bets) >= 2:
last = recent_bets[0]
prev = recent_bets[1]
if prev.result == "loss" and last.created_at and prev.created_at:
diff = (last.created_at - prev.created_at).total_seconds()
if diff < 1800: # 30 min
score += 30
factors.append("Tentativa de recuperação após perda")
# 4. Frequência alta (>5 apostas em 24h) +20
day_ago = now - timedelta(hours=24)
count_result = await db.execute(
select(func.count()).select_from(Bet).where(Bet.user_id == user_id, Bet.created_at >= day_ago)
)
bet_count = count_result.scalar() or 0
if bet_count > 5:
score += 20
factors.append(f"Alta frequência: {bet_count} apostas em 24h")
# 5. Limite estourado +25
br_result = await db.execute(select(Bankroll).where(Bankroll.user_id == user_id))
bankroll = br_result.scalar_one_or_none()
if bankroll and float(bankroll.month_spent) > float(bankroll.monthly_budget):
score += 25
factors.append("Limite mensal ultrapassado")
# 6. Emoção negativa +15
if recent_bets and recent_bets[0].emotion in ["😤", "😰", "tilt", "ansioso", "frustrado"]:
score += 15
factors.append("Emoção negativa detectada")
# 7. Sequência de perdas +20
loss_streak = 0
for b in recent_bets:
if b.result == "loss":
loss_streak += 1
else:
break
if loss_streak >= 3:
score += 20
factors.append(f"Sequência de {loss_streak} perdas consecutivas")
if score <= 30:
level = "green"
elif score <= 60:
level = "yellow"
else:
level = "red"
return {"score": score, "level": level, "factors": factors}

View File

41
backend/app/utils/auth.py Normal file
View File

@@ -0,0 +1,41 @@
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.config import settings
from app.database import get_db
from app.models.user import User
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
async def get_current_user(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)) -> User:
credentials_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
user_id: str = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if user is None:
raise credentials_exception
return user