Initial commit - MIDAS App educação financeira para apostadores (FastAPI + Next.js)
This commit is contained in:
0
backend/app/__init__.py
Normal file
0
backend/app/__init__.py
Normal file
12
backend/app/config.py
Normal file
12
backend/app/config.py
Normal 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
17
backend/app/database.py
Normal 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
33
backend/app/main.py
Normal 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"}
|
||||
0
backend/app/models/__init__.py
Normal file
0
backend/app/models/__init__.py
Normal file
27
backend/app/models/achievement.py
Normal file
27
backend/app/models/achievement.py
Normal 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")
|
||||
19
backend/app/models/alert.py
Normal file
19
backend/app/models/alert.py
Normal 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")
|
||||
23
backend/app/models/bankroll.py
Normal file
23
backend/app/models/bankroll.py
Normal 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
24
backend/app/models/bet.py
Normal 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")
|
||||
27
backend/app/models/lesson.py
Normal file
27
backend/app/models/lesson.py
Normal 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")
|
||||
24
backend/app/models/user.py
Normal file
24
backend/app/models/user.py
Normal 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")
|
||||
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)
|
||||
0
backend/app/services/__init__.py
Normal file
0
backend/app/services/__init__.py
Normal file
40
backend/app/services/gamification.py
Normal file
40
backend/app/services/gamification.py
Normal 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
|
||||
79
backend/app/services/risk_engine.py
Normal file
79
backend/app/services/risk_engine.py
Normal 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}
|
||||
0
backend/app/utils/__init__.py
Normal file
0
backend/app/utils/__init__.py
Normal file
41
backend/app/utils/auth.py
Normal file
41
backend/app/utils/auth.py
Normal 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
|
||||
10
backend/requirements.txt
Normal file
10
backend/requirements.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
fastapi==0.109.0
|
||||
uvicorn[standard]==0.27.0
|
||||
sqlalchemy[asyncio]==2.0.25
|
||||
asyncpg==0.29.0
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
bcrypt==4.1.2
|
||||
pydantic-settings==2.1.0
|
||||
httpx==0.26.0
|
||||
python-multipart==0.0.6
|
||||
152
backend/seed_data.py
Normal file
152
backend/seed_data.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""Seed MIDAS database with demo data"""
|
||||
import asyncio
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from app.database import engine, async_session, Base
|
||||
from app.models.user import User
|
||||
from app.models.bet import Bet
|
||||
from app.models.bankroll import Bankroll
|
||||
from app.models.alert import Alert
|
||||
from app.models.achievement import Achievement, UserAchievement
|
||||
from app.models.lesson import Lesson, UserLesson
|
||||
from app.utils.auth import hash_password
|
||||
|
||||
ACHIEVEMENTS = [
|
||||
{"name": "Primeiro Passo", "description": "Registrou sua primeira aposta", "icon": "👣", "category": "beginner", "requirement_type": "bets", "requirement_value": 1, "points": 10},
|
||||
{"name": "Estudante", "description": "Completou sua primeira lição", "icon": "📚", "category": "education", "requirement_type": "lessons", "requirement_value": 1, "points": 15},
|
||||
{"name": "Disciplinado", "description": "5 dias sem aposta impulsiva", "icon": "🧘", "category": "discipline", "requirement_type": "streak", "requirement_value": 5, "points": 25},
|
||||
{"name": "Analista", "description": "Registrou 10 apostas", "icon": "📊", "category": "beginner", "requirement_type": "bets", "requirement_value": 10, "points": 20},
|
||||
{"name": "Scholar", "description": "Completou 5 lições", "icon": "🎓", "category": "education", "requirement_type": "lessons", "requirement_value": 5, "points": 30},
|
||||
{"name": "Consistente", "description": "30 dias usando o MIDAS", "icon": "📅", "category": "discipline", "requirement_type": "days", "requirement_value": 30, "points": 50},
|
||||
{"name": "Mestre da Banca", "description": "Não estourou o limite por 30 dias", "icon": "💰", "category": "bankroll", "requirement_type": "bankroll_days", "requirement_value": 30, "points": 40},
|
||||
{"name": "Zen", "description": "10 dias seguidos sem aposta impulsiva", "icon": "☯️", "category": "discipline", "requirement_type": "streak", "requirement_value": 10, "points": 35},
|
||||
{"name": "Veterano", "description": "50 apostas registradas", "icon": "⭐", "category": "beginner", "requirement_type": "bets", "requirement_value": 50, "points": 30},
|
||||
{"name": "Iluminado", "description": "Completou todas as lições", "icon": "💡", "category": "education", "requirement_type": "lessons", "requirement_value": 8, "points": 50},
|
||||
]
|
||||
|
||||
LESSONS = [
|
||||
{"title": "O que é Valor Esperado", "content": "Valor Esperado (EV) é o conceito mais importante em apostas. É a média de quanto você ganharia ou perderia por aposta se repetisse a mesma aposta infinitas vezes.\n\n**Fórmula:** EV = (Probabilidade de ganhar × Lucro) - (Probabilidade de perder × Valor apostado)\n\n**Exemplo:** Aposta de R$100 em odds 2.0 com 50% de chance real:\nEV = (0.50 × R$100) - (0.50 × R$100) = R$0\n\nSe a casa oferece odds 1.90 para o mesmo evento:\nEV = (0.50 × R$90) - (0.50 × R$100) = -R$5\n\nIsso significa que, em média, você perde R$5 por aposta. Com o tempo, isso se acumula.", "category": "fundamentos", "difficulty": "iniciante", "duration_min": 8, "order_num": 1},
|
||||
{"title": "Como a Casa Sempre Ganha", "content": "A margem da casa (vig/juice) é como as casas de apostas garantem lucro.\n\n**Como funciona:** Em um cara ou coroa justo, as odds deveriam ser 2.0 para ambos os lados. Mas a casa oferece 1.90 para cada lado.\n\nSe 100 pessoas apostam R$100 em cara e 100 em coroa:\n- Casa recebe: R$20.000\n- Casa paga: 100 × R$190 = R$19.000\n- Lucro da casa: R$1.000 (5%)\n\n**A margem média das casas:**\n- Futebol: 5-8%\n- Tênis: 6-8%\n- Basquete: 4-6%\n\nNão importa quem ganha — a casa SEMPRE lucra no longo prazo.", "category": "fundamentos", "difficulty": "iniciante", "duration_min": 6, "order_num": 2},
|
||||
{"title": "Gestão de Banca 101", "content": "Gestão de banca é a skill #1 que separa apostadores disciplinados de apostadores que quebram.\n\n**Regras fundamentais:**\n1. **Defina um orçamento mensal** que NÃO comprometa suas contas\n2. **Máximo 2-5% da banca por aposta** — NUNCA mais que 10%\n3. **Nunca aposte dinheiro do aluguel/comida/contas**\n4. **Separe o dinheiro de apostas** do dinheiro do dia-a-dia\n\n**Exemplo prático:**\n- Banca mensal: R$500\n- Max por aposta (5%): R$25\n- Se perder 50% da banca: PARE e reavalie\n\n**A regra de ouro:** Se você não pode perder esse dinheiro sem afetar sua vida, NÃO aposte.", "category": "gestão", "difficulty": "iniciante", "duration_min": 7, "order_num": 3},
|
||||
{"title": "Vieses Cognitivos nas Apostas", "content": "Seu cérebro é seu pior inimigo nas apostas. Conheça os vieses:\n\n**1. Falácia do Jogador:** 'Saiu vermelho 5 vezes, agora vai sair preto!' — ERRADO. Cada evento é independente.\n\n**2. Viés de Confirmação:** Você lembra das apostas que ganhou e esquece as que perdeu.\n\n**3. Excesso de Confiança:** Após uma sequência de vitórias, você acha que 'entende' do assunto e aumenta os valores.\n\n**4. Aversão à Perda:** A dor de perder R$100 é 2x mais intensa que a alegria de ganhar R$100.\n\n**5. Efeito Ancoragem:** Se a odd era 3.0 e caiu para 2.5, parece 'ruim' — mas pode ser o valor justo.\n\n**Como se proteger:** Tenha regras fixas ANTES de apostar. Emoção é o inimigo.", "category": "psicologia", "difficulty": "intermediário", "duration_min": 10, "order_num": 4},
|
||||
{"title": "Quando Parar", "content": "Saber quando parar é mais importante que saber quando apostar.\n\n**Sinais de que você deve parar AGORA:**\n🔴 Apostando para recuperar perdas\n🔴 Aumentando valores depois de perder\n🔴 Apostando após beber ou com raiva\n🔴 Escondendo apostas de família/amigos\n🔴 Pegando dinheiro emprestado para apostar\n🔴 Pensando em apostas o tempo todo\n\n**Regras práticas:**\n- Definiu o limite diário? Atingiu = PAROU\n- Perdeu 3 seguidas? PAROU por hoje\n- Está emocional? NÃO aposte\n- São 23h+? Melhor dormir\n\n**Lembre-se:** As casas de apostas estão abertas 24/7. Sempre haverá 'a próxima oportunidade'. Não precisa ser AGORA.", "category": "disciplina", "difficulty": "iniciante", "duration_min": 6, "order_num": 5},
|
||||
{"title": "Recuperação é Armadilha", "content": "O 'chasing losses' (tentar recuperar perdas) é o comportamento mais destrutivo nas apostas.\n\n**O ciclo da destruição:**\n1. Perde R$50\n2. Aposta R$100 para recuperar\n3. Perde de novo → agora está -R$150\n4. Aposta R$200 'para empatar'\n5. Perde → -R$350 em um dia\n\n**Por que seu cérebro faz isso:**\n- Aversão à perda: quer 'zerar' o dia\n- Ilusão de controle: 'agora eu sei'\n- Dopamina: a possibilidade de recuperar é excitante\n\n**A verdade cruel:** Cada aposta é INDEPENDENTE. Suas perdas passadas não aumentam suas chances futuras.\n\n**Solução:** Aceite a perda. Feche o app. Vá fazer outra coisa. R$50 perdidos são R$50. Não transforme em R$350.", "category": "psicologia", "difficulty": "intermediário", "duration_min": 8, "order_num": 6},
|
||||
{"title": "Bankroll Management Avançado", "content": "Estratégias avançadas de gestão de banca:\n\n**1. Método de Unidades:**\n- 1 unidade = 1-2% da banca\n- Aposta normal: 1 unidade\n- Aposta com edge claro: 2 unidades\n- NUNCA mais que 3 unidades\n\n**2. Critério de Kelly:**\nKelly % = (bp - q) / b\n- b = odds decimais - 1\n- p = probabilidade real de ganhar\n- q = 1 - p\n\nExemplo: Odds 2.5, chance real 45%\nKelly = (1.5 × 0.45 - 0.55) / 1.5 = 8.3%\nUse 1/4 Kelly na prática = ~2%\n\n**3. Flat Betting:**\nAposta o mesmo valor SEMPRE. Simples e eficaz.\n\n**4. Regra do Drawdown:**\n- Perdeu 20% da banca? Reduza unidade pela metade\n- Perdeu 50%? PARE e reavalie", "category": "gestão", "difficulty": "avançado", "duration_min": 12, "order_num": 7},
|
||||
{"title": "Apostas Responsáveis", "content": "Apostar pode ser entretenimento — mas precisa ser com RESPONSABILIDADE.\n\n**Princípios do MIDAS:**\n✅ Aposte apenas dinheiro que pode perder\n✅ Defina limites ANTES de começar\n✅ Registre TODAS as apostas (é pra isso que o MIDAS existe)\n✅ Analise seus padrões regularmente\n✅ Faça pausas regulares\n✅ Converse abertamente sobre apostas\n\n**Recursos de ajuda:**\n📞 CVV: 188 (24h)\n📱 Jogadores Anônimos: www.jogadoresanonimos.org.br\n📋 Teste SOGS: avalie se suas apostas são problemáticas\n\n**O MIDAS existe para te ajudar a ter CONSCIÊNCIA.** Não somos uma plataforma de apostas. Somos sua ferramenta de autoconhecimento financeiro.\n\n**Se as apostas estão causando sofrimento, procure ajuda profissional. Não há vergonha nisso.**", "category": "responsabilidade", "difficulty": "iniciante", "duration_min": 5, "order_num": 8},
|
||||
]
|
||||
|
||||
DEMO_BETS = [
|
||||
{"sport": "futebol", "event_name": "Flamengo vs Palmeiras", "platform": "Bet365", "amount": 25, "odds": 2.10, "result": "win", "emotion": "😎"},
|
||||
{"sport": "futebol", "event_name": "Real Madrid vs Barcelona", "platform": "Betano", "amount": 30, "odds": 1.85, "result": "win", "emotion": "😎"},
|
||||
{"sport": "futebol", "event_name": "Liverpool vs Man City", "platform": "Bet365", "amount": 20, "odds": 3.20, "result": "loss", "emotion": "😰"},
|
||||
{"sport": "UFC", "event_name": "UFC 310: Pantoja vs Asakura", "platform": "Betano", "amount": 15, "odds": 1.65, "result": "win", "emotion": "🤑"},
|
||||
{"sport": "basquete", "event_name": "Lakers vs Celtics", "platform": "Bet365", "amount": 25, "odds": 2.40, "result": "loss", "emotion": "😤"},
|
||||
{"sport": "futebol", "event_name": "Corinthians vs São Paulo", "platform": "Betano", "amount": 20, "odds": 2.00, "result": "win", "emotion": "😎"},
|
||||
{"sport": "UFC", "event_name": "UFC 311: Makhachev vs Tsarukyan", "platform": "Bet365", "amount": 30, "odds": 1.45, "result": "win", "emotion": "😎"},
|
||||
{"sport": "futebol", "event_name": "Argentina vs Brasil", "platform": "Betano", "amount": 50, "odds": 2.80, "result": "loss", "emotion": "😤"},
|
||||
{"sport": "basquete", "event_name": "Warriors vs Bucks", "platform": "Bet365", "amount": 15, "odds": 1.90, "result": "win", "emotion": "😎"},
|
||||
{"sport": "futebol", "event_name": "PSG vs Bayern", "platform": "Bet365", "amount": 20, "odds": 2.50, "result": "loss", "emotion": "😰"},
|
||||
{"sport": "futebol", "event_name": "Botafogo vs Fluminense", "platform": "Betano", "amount": 15, "odds": 1.75, "result": "win", "emotion": "🤑"},
|
||||
{"sport": "UFC", "event_name": "UFC Fight Night: Moreno vs Royval", "platform": "Bet365", "amount": 20, "odds": 2.20, "result": "loss", "emotion": "😰"},
|
||||
{"sport": "basquete", "event_name": "Nets vs Knicks", "platform": "Betano", "amount": 25, "odds": 1.95, "result": "win", "emotion": "😎"},
|
||||
{"sport": "futebol", "event_name": "Inter vs Grêmio", "platform": "Bet365", "amount": 20, "odds": 2.05, "result": "win", "emotion": "😎"},
|
||||
{"sport": "futebol", "event_name": "Chelsea vs Arsenal", "platform": "Betano", "amount": 35, "odds": 3.00, "result": "loss", "emotion": "😤"},
|
||||
{"sport": "basquete", "event_name": "Heat vs 76ers", "platform": "Bet365", "amount": 20, "odds": 2.15, "result": "pending", "emotion": "😎"},
|
||||
{"sport": "UFC", "event_name": "UFC 312: Du Plessis vs Strickland", "platform": "Betano", "amount": 25, "odds": 1.80, "result": "pending", "emotion": "🤑"},
|
||||
{"sport": "futebol", "event_name": "Atlético MG vs Cruzeiro", "platform": "Bet365", "amount": 15, "odds": 2.30, "result": "win", "emotion": "😎"},
|
||||
{"sport": "futebol", "event_name": "Juventus vs Milan", "platform": "Betano", "amount": 20, "odds": 2.60, "result": "loss", "emotion": "😰"},
|
||||
{"sport": "basquete", "event_name": "Suns vs Nuggets", "platform": "Bet365", "amount": 30, "odds": 2.00, "result": "pending", "emotion": "😎"},
|
||||
]
|
||||
|
||||
async def seed():
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
async with async_session() as db:
|
||||
# Create demo user
|
||||
user = User(
|
||||
email="demo@midas.com",
|
||||
password_hash=hash_password("Midas@2026"),
|
||||
name="Demo User",
|
||||
plan="premium",
|
||||
streak_days=7,
|
||||
total_points=185,
|
||||
risk_level="yellow"
|
||||
)
|
||||
db.add(user)
|
||||
await db.flush()
|
||||
|
||||
# Bankroll
|
||||
bankroll = Bankroll(
|
||||
user_id=user.id,
|
||||
monthly_budget=500,
|
||||
weekly_limit=150,
|
||||
daily_limit=50,
|
||||
bet_max_pct=5,
|
||||
month_spent=245,
|
||||
week_spent=85,
|
||||
day_spent=30
|
||||
)
|
||||
db.add(bankroll)
|
||||
|
||||
# Achievements
|
||||
achievement_objs = []
|
||||
for a in ACHIEVEMENTS:
|
||||
obj = Achievement(**a)
|
||||
db.add(obj)
|
||||
achievement_objs.append(obj)
|
||||
await db.flush()
|
||||
|
||||
# Unlock first 5 achievements for demo user
|
||||
for a in achievement_objs[:5]:
|
||||
ua = UserAchievement(user_id=user.id, achievement_id=a.id)
|
||||
db.add(ua)
|
||||
|
||||
# Lessons
|
||||
lesson_objs = []
|
||||
for l in LESSONS:
|
||||
obj = Lesson(**l)
|
||||
db.add(obj)
|
||||
lesson_objs.append(obj)
|
||||
await db.flush()
|
||||
|
||||
# Complete first 3 lessons for demo
|
||||
for l in lesson_objs[:3]:
|
||||
ul = UserLesson(user_id=user.id, lesson_id=l.id)
|
||||
db.add(ul)
|
||||
|
||||
# Bets (spread over last 30 days)
|
||||
now = datetime.utcnow()
|
||||
for i, b in enumerate(DEMO_BETS):
|
||||
days_ago = 30 - (i * 1.5)
|
||||
created = now - timedelta(days=days_ago, hours=random.randint(8, 22))
|
||||
profit = 0
|
||||
if b["result"] == "win":
|
||||
profit = b["amount"] * (b["odds"] - 1)
|
||||
elif b["result"] == "loss":
|
||||
profit = -b["amount"]
|
||||
bet = Bet(
|
||||
user_id=user.id, sport=b["sport"], event_name=b["event_name"],
|
||||
platform=b["platform"], amount=b["amount"], odds=b["odds"],
|
||||
result=b["result"], profit=profit, emotion=b["emotion"],
|
||||
created_at=created
|
||||
)
|
||||
db.add(bet)
|
||||
|
||||
# Some alerts
|
||||
alerts = [
|
||||
Alert(user_id=user.id, type="recovery", severity="red", message="Aposta de recuperação detectada após perda em Liverpool vs Man City"),
|
||||
Alert(user_id=user.id, type="limit", severity="yellow", message="Você já usou 49% do limite mensal"),
|
||||
Alert(user_id=user.id, type="streak", severity="green", message="Parabéns! 7 dias sem aposta impulsiva 🔥"),
|
||||
]
|
||||
for a in alerts:
|
||||
db.add(a)
|
||||
|
||||
await db.commit()
|
||||
print("✅ Seed data created successfully!")
|
||||
print(f" User: demo@midas.com / Midas@2026")
|
||||
print(f" {len(DEMO_BETS)} bets, {len(ACHIEVEMENTS)} achievements, {len(LESSONS)} lessons")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed())
|
||||
Reference in New Issue
Block a user