Initial commit - MIDAS App educação financeira para apostadores (FastAPI + Next.js)
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
node_modules/
|
||||||
|
.next/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
venv/
|
||||||
|
.env
|
||||||
|
dist/
|
||||||
|
.env.*
|
||||||
|
*.egg-info/
|
||||||
|
.venv/
|
||||||
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())
|
||||||
407
docs/ARQUITETURA-TECNICA.md
Normal file
407
docs/ARQUITETURA-TECNICA.md
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
# MIDAS — Arquitetura Técnica
|
||||||
|
## App de Educação Financeira para Apostadores | PWA
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Stack
|
||||||
|
|
||||||
|
| Camada | Tecnologia |
|
||||||
|
|--------|-----------|
|
||||||
|
| Frontend | Next.js 14 (PWA) + TailwindCSS |
|
||||||
|
| Backend | FastAPI (Python 3.12) |
|
||||||
|
| Banco | PostgreSQL 16 + Redis 7 |
|
||||||
|
| IA | GPT-4o-mini (análise de padrões) |
|
||||||
|
| Auth | JWT + bcrypt |
|
||||||
|
| Deploy | DigitalOcean (jarvis-do) |
|
||||||
|
| Pagamento | Stripe |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Estrutura
|
||||||
|
|
||||||
|
```
|
||||||
|
midas/
|
||||||
|
├── backend/
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── main.py
|
||||||
|
│ │ ├── config.py
|
||||||
|
│ │ ├── database.py
|
||||||
|
│ │ ├── models/
|
||||||
|
│ │ │ ├── user.py # Usuário + plano
|
||||||
|
│ │ │ ├── bet.py # Apostas registradas
|
||||||
|
│ │ │ ├── bankroll.py # Banca + limites
|
||||||
|
│ │ │ ├── alert.py # Alertas de risco
|
||||||
|
│ │ │ ├── achievement.py # Badges/conquistas
|
||||||
|
│ │ │ └── lesson.py # Conteúdo educativo
|
||||||
|
│ │ ├── routers/
|
||||||
|
│ │ │ ├── auth.py # Login/registro
|
||||||
|
│ │ │ ├── bets.py # CRUD apostas
|
||||||
|
│ │ │ ├── bankroll.py # Gestão de banca
|
||||||
|
│ │ │ ├── dashboard.py # Dashboard principal
|
||||||
|
│ │ │ ├── alerts.py # Alertas IA
|
||||||
|
│ │ │ ├── achievements.py # Gamificação
|
||||||
|
│ │ │ ├── lessons.py # Educação
|
||||||
|
│ │ │ ├── reports.py # Relatórios
|
||||||
|
│ │ │ └── billing.py # Stripe
|
||||||
|
│ │ ├── services/
|
||||||
|
│ │ │ ├── analyzer.py # IA análise de padrões
|
||||||
|
│ │ │ ├── risk_engine.py # Motor de risco (semáforo)
|
||||||
|
│ │ │ ├── bankroll_calc.py # Calculadora de banca
|
||||||
|
│ │ │ ├── gamification.py # Engine de badges
|
||||||
|
│ │ │ └── insights.py # Geração de insights IA
|
||||||
|
│ │ └── utils/
|
||||||
|
│ │ ├── auth.py
|
||||||
|
│ │ └── rate_limit.py
|
||||||
|
│ ├── requirements.txt
|
||||||
|
│ └── seed_data.py
|
||||||
|
├── frontend/ # Next.js 14 PWA
|
||||||
|
│ ├── src/app/
|
||||||
|
│ │ ├── page.tsx # Landing
|
||||||
|
│ │ ├── login/
|
||||||
|
│ │ ├── register/
|
||||||
|
│ │ ├── dashboard/ # Dashboard principal
|
||||||
|
│ │ ├── bets/ # Registrar/listar apostas
|
||||||
|
│ │ ├── bankroll/ # Gestão de banca
|
||||||
|
│ │ ├── alerts/ # Alertas de risco
|
||||||
|
│ │ ├── achievements/ # Badges e ranking
|
||||||
|
│ │ ├── lessons/ # Mini-cursos
|
||||||
|
│ │ ├── reports/ # Relatórios mensais
|
||||||
|
│ │ └── premium/
|
||||||
|
│ └── src/components/
|
||||||
|
│ ├── Navbar.tsx
|
||||||
|
│ ├── RiskSemaphore.tsx # 🟢🟡🔴
|
||||||
|
│ ├── BankrollGauge.tsx # Gauge de banca
|
||||||
|
│ ├── BetCard.tsx
|
||||||
|
│ ├── AchievementBadge.tsx
|
||||||
|
│ ├── InsightCard.tsx
|
||||||
|
│ └── StatsChart.tsx
|
||||||
|
└── docs/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Modelo de Dados
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Usuários
|
||||||
|
CREATE TABLE users (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
name VARCHAR(100),
|
||||||
|
plan VARCHAR(20) DEFAULT 'free',
|
||||||
|
streak_days INT DEFAULT 0,
|
||||||
|
total_points INT DEFAULT 0,
|
||||||
|
risk_level VARCHAR(10) DEFAULT 'green',
|
||||||
|
stripe_customer_id VARCHAR(100),
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Banca (bankroll)
|
||||||
|
CREATE TABLE bankrolls (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID REFERENCES users(id),
|
||||||
|
monthly_budget DECIMAL(10,2) NOT NULL,
|
||||||
|
weekly_limit DECIMAL(10,2),
|
||||||
|
daily_limit DECIMAL(10,2),
|
||||||
|
bet_max_pct DECIMAL(5,2) DEFAULT 5.0,
|
||||||
|
current_balance DECIMAL(10,2) DEFAULT 0,
|
||||||
|
month_spent DECIMAL(10,2) DEFAULT 0,
|
||||||
|
week_spent DECIMAL(10,2) DEFAULT 0,
|
||||||
|
day_spent DECIMAL(10,2) DEFAULT 0,
|
||||||
|
reset_day INT DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Apostas registradas
|
||||||
|
CREATE TABLE bets (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID REFERENCES users(id),
|
||||||
|
sport VARCHAR(50),
|
||||||
|
event_name VARCHAR(255),
|
||||||
|
platform VARCHAR(100),
|
||||||
|
amount DECIMAL(10,2) NOT NULL,
|
||||||
|
odds DECIMAL(8,3),
|
||||||
|
result VARCHAR(10),
|
||||||
|
profit DECIMAL(10,2),
|
||||||
|
bet_type VARCHAR(50),
|
||||||
|
is_impulsive BOOLEAN DEFAULT FALSE,
|
||||||
|
emotion VARCHAR(50),
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_bets_user ON bets(user_id, created_at DESC);
|
||||||
|
|
||||||
|
-- Alertas
|
||||||
|
CREATE TABLE alerts (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID REFERENCES users(id),
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
severity VARCHAR(10) NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
ai_analysis TEXT,
|
||||||
|
is_read BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Conquistas
|
||||||
|
CREATE TABLE achievements (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
icon VARCHAR(10),
|
||||||
|
category VARCHAR(50),
|
||||||
|
requirement_type VARCHAR(50),
|
||||||
|
requirement_value INT,
|
||||||
|
points INT DEFAULT 10
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_achievements (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID REFERENCES users(id),
|
||||||
|
achievement_id UUID REFERENCES achievements(id),
|
||||||
|
unlocked_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Lições
|
||||||
|
CREATE TABLE lessons (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
category VARCHAR(50),
|
||||||
|
difficulty VARCHAR(20),
|
||||||
|
duration_min INT DEFAULT 5,
|
||||||
|
order_num INT,
|
||||||
|
is_premium BOOLEAN DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_lessons (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID REFERENCES users(id),
|
||||||
|
lesson_id UUID REFERENCES lessons(id),
|
||||||
|
completed_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Fluxo Principal
|
||||||
|
|
||||||
|
```
|
||||||
|
📱 Usuário abre o MIDAS
|
||||||
|
↓
|
||||||
|
📊 Dashboard mostra:
|
||||||
|
├─ Semáforo de risco: 🟢 Verde / 🟡 Amarelo / 🔴 Vermelho
|
||||||
|
├─ Banca restante: R$280 / R$500 (gauge visual)
|
||||||
|
├─ Streak: 🔥 5 dias sem aposta impulsiva
|
||||||
|
├─ Últimas apostas com resultado
|
||||||
|
└─ Insights IA: "Você perde 73% mais em jogos noturnos"
|
||||||
|
↓
|
||||||
|
➕ Registrar aposta:
|
||||||
|
├─ Esporte, evento, plataforma, valor, odds
|
||||||
|
├─ "Como você está se sentindo?" (😎😤😰🤑)
|
||||||
|
├─ Checklist pré-aposta: ✅ Dentro do limite? ✅ Analisou? ✅ Não é recuperação?
|
||||||
|
└─ Se valor > limite → alerta + confirmação extra
|
||||||
|
↓
|
||||||
|
🧠 IA analisa padrão:
|
||||||
|
├─ Horário (madrugada = risco)
|
||||||
|
├─ Sequência (3+ perdas seguidas = alerta)
|
||||||
|
├─ Valor crescente (escalonamento = vício)
|
||||||
|
├─ Emoção (apostando com raiva/frustração)
|
||||||
|
└─ Frequência (todo dia = dependência)
|
||||||
|
↓
|
||||||
|
🚨 Se risco detectado → Alerta:
|
||||||
|
├─ Push notification
|
||||||
|
├─ Mensagem educativa
|
||||||
|
├─ Sugestão de pausa
|
||||||
|
└─ Link para ajuda profissional (CVV, GREA-USP)
|
||||||
|
↓
|
||||||
|
🏆 Gamificação:
|
||||||
|
├─ Badge: "Disciplina de Ferro" (7 dias sem impulsiva)
|
||||||
|
├─ Badge: "Analista" (completou 5 lições)
|
||||||
|
├─ Ranking semanal de disciplina
|
||||||
|
└─ Pontos → desbloqueia conteúdo premium
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. API Endpoints
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
```
|
||||||
|
POST /api/auth/register
|
||||||
|
POST /api/auth/login
|
||||||
|
GET /api/auth/me
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
```
|
||||||
|
GET /api/dashboard # Dados principais (banca, risco, streak, insights)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Apostas
|
||||||
|
```
|
||||||
|
POST /api/bets # Registrar aposta
|
||||||
|
GET /api/bets # Listar apostas
|
||||||
|
GET /api/bets/stats # Estatísticas (win rate, ROI, por esporte)
|
||||||
|
PATCH /api/bets/:id/result # Registrar resultado (win/loss)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Banca
|
||||||
|
```
|
||||||
|
GET /api/bankroll # Status da banca
|
||||||
|
PUT /api/bankroll # Configurar limites
|
||||||
|
GET /api/bankroll/check # Verificar se aposta está dentro do limite
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alertas
|
||||||
|
```
|
||||||
|
GET /api/alerts # Listar alertas
|
||||||
|
PATCH /api/alerts/:id/read # Marcar como lido
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gamificação
|
||||||
|
```
|
||||||
|
GET /api/achievements # Todas as conquistas
|
||||||
|
GET /api/achievements/mine # Minhas conquistas
|
||||||
|
GET /api/leaderboard # Ranking
|
||||||
|
```
|
||||||
|
|
||||||
|
### Educação
|
||||||
|
```
|
||||||
|
GET /api/lessons # Listar lições
|
||||||
|
GET /api/lessons/:id # Conteúdo da lição
|
||||||
|
POST /api/lessons/:id/complete # Marcar como concluída
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relatórios
|
||||||
|
```
|
||||||
|
GET /api/reports/weekly # Relatório semanal
|
||||||
|
GET /api/reports/monthly # Relatório mensal
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Motor de Risco (IA)
|
||||||
|
|
||||||
|
### Semáforo
|
||||||
|
```
|
||||||
|
🟢 VERDE — Apostando dentro dos limites, padrão saudável
|
||||||
|
🟡 AMARELO — 1-2 sinais de alerta detectados
|
||||||
|
🔴 VERMELHO — Padrão de risco/vício identificado
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sinais de Alerta (pesos)
|
||||||
|
| Sinal | Peso | Descrição |
|
||||||
|
|-------|------|-----------|
|
||||||
|
| Horário noturno (23h-5h) | 15 | Apostas de madrugada |
|
||||||
|
| Valor crescente | 25 | Cada aposta maior que a anterior |
|
||||||
|
| Recuperação | 30 | Apostar logo após perda pra "recuperar" |
|
||||||
|
| Frequência alta | 20 | +3 apostas/dia |
|
||||||
|
| Limite estourado | 25 | Passou do orçamento definido |
|
||||||
|
| Emoção negativa | 15 | Apostando com raiva/frustração |
|
||||||
|
| Sequência de perdas | 20 | 3+ perdas seguidas sem parar |
|
||||||
|
|
||||||
|
**Score**: soma dos pesos → 0-30 verde, 31-60 amarelo, 61+ vermelho
|
||||||
|
|
||||||
|
### Prompt IA (insights)
|
||||||
|
```python
|
||||||
|
INSIGHT_PROMPT = """
|
||||||
|
Analise o histórico de apostas deste usuário e gere insights.
|
||||||
|
|
||||||
|
DADOS:
|
||||||
|
- Total apostado mês: R${total_month}
|
||||||
|
- Win rate: {win_rate}%
|
||||||
|
- Esportes: {sports}
|
||||||
|
- Horários mais comuns: {hours}
|
||||||
|
- Padrão de valores: {amounts}
|
||||||
|
- Sequências de perda: {loss_streaks}
|
||||||
|
- Emoções registradas: {emotions}
|
||||||
|
|
||||||
|
Gere 3 insights em JSON:
|
||||||
|
[
|
||||||
|
{"icon": "💡", "title": "título curto", "text": "insight prático em 1 frase"},
|
||||||
|
]
|
||||||
|
|
||||||
|
Seja direto, honesto e educativo. Português BR.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Gamificação — Badges
|
||||||
|
|
||||||
|
| Badge | Requisito | Pontos |
|
||||||
|
|-------|-----------|--------|
|
||||||
|
| 🌱 Primeiro Passo | Registrar primeira aposta | 10 |
|
||||||
|
| 📚 Estudante | Completar 3 lições | 20 |
|
||||||
|
| 🎯 Disciplinado | 7 dias dentro do limite | 50 |
|
||||||
|
| 🔥 Streak de Ferro | 30 dias sem aposta impulsiva | 100 |
|
||||||
|
| 🧠 Analista | Registrar 50 apostas com análise | 30 |
|
||||||
|
| 🛡️ Autocontrole | Cancelar aposta após alerta | 40 |
|
||||||
|
| 📊 Consciente | Ver relatório mensal 3x | 15 |
|
||||||
|
| 👑 Mestre da Banca | ROI positivo por 3 meses | 200 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Design System
|
||||||
|
|
||||||
|
### Paleta
|
||||||
|
- **Dourado:** #D4A843 (premium, dinheiro, Midas)
|
||||||
|
- **Azul escuro:** #0F172A (confiança, seriedade)
|
||||||
|
- **Verde:** #10B981 (seguro, aprovado)
|
||||||
|
- **Amarelo:** #FBBF24 (atenção)
|
||||||
|
- **Vermelho:** #EF4444 (perigo, stop)
|
||||||
|
- **Branco:** #F8FAFC
|
||||||
|
|
||||||
|
### Identidade
|
||||||
|
- Logo: Coroa dourada + "MIDAS"
|
||||||
|
- Slogan: "Aposte com consciência"
|
||||||
|
- Font: Inter (body) + Cabinet Grotesk (display)
|
||||||
|
- Mobile-first, PWA instalável
|
||||||
|
- Dark mode como padrão
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Portas & Deploy
|
||||||
|
|
||||||
|
| Serviço | Porta | PM2 Name |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| Backend FastAPI | 8095 | midas-backend |
|
||||||
|
| Frontend Next.js | 3085 | midas-frontend |
|
||||||
|
|
||||||
|
- **URL**: midas.aivertice.com
|
||||||
|
- **DB**: PostgreSQL `midas` / user `midas`
|
||||||
|
- **Deploy**: jarvis-do (198.199.84.130)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Roadmap
|
||||||
|
|
||||||
|
### MVP — Semanas 1-2
|
||||||
|
- [ ] Auth (registro/login)
|
||||||
|
- [ ] Dashboard com semáforo de risco
|
||||||
|
- [ ] Registrar apostas manualmente
|
||||||
|
- [ ] Gestão de banca (definir limites)
|
||||||
|
- [ ] Alertas básicos (limite estourado, frequência alta)
|
||||||
|
- [ ] 5 lições educativas
|
||||||
|
- [ ] 5 badges iniciais
|
||||||
|
- [ ] PWA instalável
|
||||||
|
- [ ] Deploy DigitalOcean
|
||||||
|
|
||||||
|
### v1.0 — Semanas 3-4
|
||||||
|
- [ ] IA insights (GPT-4o-mini)
|
||||||
|
- [ ] Relatórios semanais/mensais
|
||||||
|
- [ ] Mais badges e ranking
|
||||||
|
- [ ] Stripe (assinatura premium)
|
||||||
|
- [ ] Push notifications
|
||||||
|
|
||||||
|
### v2.0 — Semanas 5-8
|
||||||
|
- [ ] Integração com casas de aposta (API)
|
||||||
|
- [ ] Compartilhar relatório (stories)
|
||||||
|
- [ ] Desafios semanais
|
||||||
|
- [ ] White-label B2B
|
||||||
|
- [ ] App nativo React Native
|
||||||
|
|
||||||
|
---
|
||||||
|
*Arquitetura JARVIS — 10/02/2026*
|
||||||
301
docs/ESTRATEGIA-NEGOCIO.md
Normal file
301
docs/ESTRATEGIA-NEGOCIO.md
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
# MIDAS — Estratégia de Negócio
|
||||||
|
|
||||||
|
> SaaS de educação financeira e gestão de banca para apostadores esportivos.
|
||||||
|
> Software puro — sem necessidade de CVM/fintech.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Business Model Canvas
|
||||||
|
|
||||||
|
| Bloco | Descrição |
|
||||||
|
|---|---|
|
||||||
|
| **Proposta de Valor** | IA que analisa padrões de aposta, alerta comportamento de risco, ensina gestão de banca e gamifica o aprendizado financeiro. Ajuda o apostador a não quebrar. |
|
||||||
|
| **Segmentos de Clientes** | **B2C:** Apostadores esportivos brasileiros (18-35 anos, classes B/C). **B2B:** Casas de aposta operando no Brasil (compliance Lei 14.790). |
|
||||||
|
| **Canais** | App mobile (iOS/Android), web app, API white-label para B2B. Aquisição via TikTok, Instagram Reels, YouTube, influencers de bet. |
|
||||||
|
| **Relacionamento** | Self-service freemium + comunidade gamificada + suporte in-app. B2B: account manager dedicado. |
|
||||||
|
| **Fontes de Receita** | Assinatura B2C (freemium/premium/family). B2B: licença por usuário ativo (MAU) + white-label fee. |
|
||||||
|
| **Recursos-Chave** | Algoritmos de IA/ML para análise de padrões, app mobile, infraestrutura cloud, equipe de produto e data science. |
|
||||||
|
| **Atividades-Chave** | Desenvolvimento de produto, treinamento de modelos de IA, aquisição de usuários, parcerias B2B, produção de conteúdo educacional. |
|
||||||
|
| **Parcerias-Chave** | Influencers de aposta esportiva, casas de aposta (Bet365, Betano, Sportingbet), produtores de conteúdo esportivo, psicólogos especializados em jogo. |
|
||||||
|
| **Estrutura de Custos** | Infra cloud (AWS/GCP), salários (dev + data science), marketing de aquisição, custo de IA (API calls), suporte. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Pricing Strategy
|
||||||
|
|
||||||
|
### B2C
|
||||||
|
|
||||||
|
| Tier | Preço | Inclui |
|
||||||
|
|---|---|---|
|
||||||
|
| **Free** | R$ 0 | Dashboard básico de banca, 3 alertas de risco/mês, conteúdo educacional limitado, ranking básico |
|
||||||
|
| **Premium** | R$ 19,90/mês | IA completa de análise de padrões, alertas ilimitados, relatórios semanais, gamificação completa, desafios, comunidade premium |
|
||||||
|
| **Family** | R$ 39,90/mês | Até 4 contas vinculadas, painel de monitoramento familiar, alertas para responsáveis, tudo do Premium |
|
||||||
|
|
||||||
|
**Estratégia:** Free generoso o suficiente para hook, mas com gate claro nos alertas de IA e relatórios. Upsell natural quando o usuário vê valor nos insights.
|
||||||
|
|
||||||
|
### B2B
|
||||||
|
|
||||||
|
| Modelo | Preço | Descrição |
|
||||||
|
|---|---|---|
|
||||||
|
| **Por MAU** | R$ 1,50 - R$ 3,00/usuário ativo/mês | SDK integrado ao app da casa de aposta. Escala degressive: >100k MAU = R$1,50; <10k MAU = R$3,00 |
|
||||||
|
| **White-label** | R$ 15.000 setup + R$ 2,00/MAU | Marca da casa de aposta, customização visual completa, dashboard admin dedicado |
|
||||||
|
| **Enterprise** | Sob consulta | Modelos de IA customizados, integração profunda, SLA dedicado |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Go-to-Market
|
||||||
|
|
||||||
|
### Fase 1 — Pré-lançamento (semanas -4 a 0)
|
||||||
|
|
||||||
|
- Landing page com waitlist + "teste gratuito do seu perfil de apostador" (quiz viral)
|
||||||
|
- 5-10 micro-influencers de bet (10k-100k seguidores) com código de acesso antecipado
|
||||||
|
- Conteúdo orgânico TikTok/Reels: "Quanto você perdeu apostando esse mês?" — formato polêmico que gera engajamento
|
||||||
|
- Grupo de Telegram beta fechado (500 pessoas)
|
||||||
|
|
||||||
|
### Fase 2 — Lançamento (semanas 0-4)
|
||||||
|
|
||||||
|
- **TikTok viral:** Série "Diário de um apostador" — mostrar dashboard real, gestão de banca, antes/depois
|
||||||
|
- **Influencers:** 3-5 influencers mid-tier (100k-500k) com deal de performance (CPA R$5-8)
|
||||||
|
- **Conteúdo polêmico:** "As casas de aposta não querem que você saiba disso" — clickbait ético sobre gestão de banca
|
||||||
|
- **Reddit/Twitter Spaces:** Participar de comunidades de aposta esportiva
|
||||||
|
|
||||||
|
### Fase 3 — Crescimento (meses 2-6)
|
||||||
|
|
||||||
|
- Programa de indicação: "Convide um amigo, ganhe 1 mês premium"
|
||||||
|
- Parcerias com podcasts de esporte (Charla, PodPah quando fala de futebol)
|
||||||
|
- Google Ads em termos de busca: "como parar de perder em aposta", "gestão de banca"
|
||||||
|
- SEO: blog com conteúdo educacional de aposta responsável
|
||||||
|
|
||||||
|
### Canais prioritários (por ROI esperado)
|
||||||
|
|
||||||
|
1. TikTok/Reels orgânico (custo zero, alcance massivo)
|
||||||
|
2. Influencers de bet com CPA (mensurável)
|
||||||
|
3. Referral program (viral loop)
|
||||||
|
4. Google Ads intent-based (alta conversão)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. B2B Pitch — Casas de Aposta
|
||||||
|
|
||||||
|
### O problema delas
|
||||||
|
|
||||||
|
A **Lei 14.790/2023** obriga operadoras a:
|
||||||
|
- Implementar mecanismos de jogo responsável
|
||||||
|
- Monitorar comportamento de risco dos usuários
|
||||||
|
- Oferecer ferramentas de autoexclusão e limites
|
||||||
|
- Reportar para a SPA (Secretaria de Prêmios e Apostas)
|
||||||
|
|
||||||
|
**Multas pesadas** para quem não cumprir. Construir internamente = caro, demorado e fora do core business.
|
||||||
|
|
||||||
|
### A solução MIDAS
|
||||||
|
|
||||||
|
> "Compliance de jogo responsável plug-and-play. SDK leve, integração em 2 semanas, relatórios prontos para a SPA."
|
||||||
|
|
||||||
|
**Pitch em 3 pontos:**
|
||||||
|
|
||||||
|
1. **Compliance instantâneo:** Nosso SDK cobre 100% dos requisitos da Lei 14.790 para jogo responsável. Reduz risco regulatório.
|
||||||
|
2. **Retenção de jogadores:** Jogador educado joga mais tempo (LTV +30% baseado em dados de mercados regulados como UK/Suécia). Apostador que quebra = churn.
|
||||||
|
3. **Custo vs build:** Build interno = 6-12 meses + equipe dedicada. MIDAS = 2 semanas de integração, R$1,50-3,00/MAU.
|
||||||
|
|
||||||
|
### Targets prioritários
|
||||||
|
|
||||||
|
| Operadora | Ângulo de entrada | Contato sugerido |
|
||||||
|
|---|---|---|
|
||||||
|
| **Bet365** | Maior do Brasil, precisa liderar em compliance. Pitch: "seja referência em jogo responsável" | Head of Compliance BR |
|
||||||
|
| **Betano** | Agressiva em marketing, precisa balancear com responsabilidade | VP de Produto |
|
||||||
|
| **Sportingbet** | Flutter Entertainment, já tem framework global — pitch de localização Brasil | Country Manager |
|
||||||
|
| **Betnacional** | Operadora brasileira, mais ágil para fechar | CEO/CTO direto |
|
||||||
|
| **EstrelaBet** | Crescimento rápido, precisa profissionalizar compliance | Head of Operations |
|
||||||
|
|
||||||
|
### Materiais necessários
|
||||||
|
|
||||||
|
- One-pager PDF (1 página, dados + proposta)
|
||||||
|
- Demo interativa do SDK (sandbox)
|
||||||
|
- Case study com dados de mercados regulados (UK Gambling Commission data)
|
||||||
|
- Proposta comercial customizada por operadora
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Unit Economics
|
||||||
|
|
||||||
|
### B2C
|
||||||
|
|
||||||
|
| Métrica | Valor estimado | Premissa |
|
||||||
|
|---|---|---|
|
||||||
|
| **CAC** | R$ 15-25 | Mix de orgânico (R$0) + paid (R$30-50). Orgânico = 60% no início |
|
||||||
|
| **ARPU** | R$ 12/mês | Mix free (70%) + premium R$19,90 (25%) + family R$39,90 (5%) |
|
||||||
|
| **Churn mensal** | 8-12% | Apps de fintech BR = ~10%. Gamificação deve reduzir para ~8% com maturidade |
|
||||||
|
| **LTV** | R$ 100-150 | ARPU R$12 / churn 10% = R$120 |
|
||||||
|
| **LTV/CAC** | 5-8x | Saudável (meta >3x) |
|
||||||
|
| **Payback** | 1,5-2 meses | CAC R$20 / ARPU R$12 = ~1,7 meses |
|
||||||
|
| **Margem bruta** | 80-85% | SaaS puro, custo principal = infra + IA API calls |
|
||||||
|
|
||||||
|
### B2B
|
||||||
|
|
||||||
|
| Métrica | Valor estimado |
|
||||||
|
|---|---|
|
||||||
|
| **CAC** | R$ 15.000-30.000 (vendas enterprise) |
|
||||||
|
| **ACV (Annual Contract Value)** | R$ 180.000 - R$ 1.800.000 (depende do MAU) |
|
||||||
|
| **Churn anual** | 5-10% (contratos anuais) |
|
||||||
|
| **LTV** | R$ 1.800.000 - R$ 18.000.000 |
|
||||||
|
| **LTV/CAC** | 60-100x |
|
||||||
|
|
||||||
|
### Breakeven estimado
|
||||||
|
|
||||||
|
- **Cenário conservador:** Mês 8-10 com ~5.000 assinantes pagos
|
||||||
|
- **Com 1 contrato B2B:** Breakeven imediato
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Roadmap de Produto
|
||||||
|
|
||||||
|
### MVP — Semanas 1-4
|
||||||
|
|
||||||
|
**Objetivo:** Validar proposta de valor com early adopters.
|
||||||
|
|
||||||
|
- [ ] Onboarding: perfil do apostador (esportes, frequência, banca média)
|
||||||
|
- [ ] Dashboard de banca: registro manual de apostas (entrada/saída)
|
||||||
|
- [ ] 3 alertas básicos de risco (perda sequencial, aumento de stake, frequência anormal)
|
||||||
|
- [ ] Conteúdo educacional: 10 artigos sobre gestão de banca
|
||||||
|
- [ ] Gamificação básica: streak de dias "no controle" + badges
|
||||||
|
- [ ] Auth (email/Google) + perfil
|
||||||
|
- [ ] Landing page + waitlist
|
||||||
|
|
||||||
|
**Stack:** React Native (Expo) + Supabase + OpenAI API
|
||||||
|
**Time:** 2 devs full-stack + 1 designer
|
||||||
|
|
||||||
|
### v1.0 — Semanas 5-8
|
||||||
|
|
||||||
|
**Objetivo:** Monetização + retenção.
|
||||||
|
|
||||||
|
- [ ] IA de análise de padrões (ML sobre histórico de apostas do usuário)
|
||||||
|
- [ ] Relatórios semanais automáticos ("Seu resumo da semana")
|
||||||
|
- [ ] Sistema de assinatura (Stripe/RevenueCat) — Free/Premium
|
||||||
|
- [ ] Comunidade in-app (feed + ranking)
|
||||||
|
- [ ] Desafios semanais gamificados ("Fique dentro do orçamento 7 dias")
|
||||||
|
- [ ] Push notifications inteligentes (alertas contextuais)
|
||||||
|
- [ ] Import de dados via screenshot OCR (ler comprovantes de aposta)
|
||||||
|
|
||||||
|
### v2.0 — Semanas 9-16
|
||||||
|
|
||||||
|
**Objetivo:** B2B + escala.
|
||||||
|
|
||||||
|
- [ ] SDK para casas de aposta (JavaScript + React Native)
|
||||||
|
- [ ] Dashboard admin B2B (métricas de jogo responsável por operadora)
|
||||||
|
- [ ] Relatórios de compliance Lei 14.790 (exportáveis para SPA)
|
||||||
|
- [ ] API de integração (REST + webhooks)
|
||||||
|
- [ ] Plano Family (painel de monitoramento)
|
||||||
|
- [ ] Modelos de IA avançados (predição de risco, segmentação comportamental)
|
||||||
|
- [ ] White-label customizável (temas, marca, idioma)
|
||||||
|
- [ ] Integração direta com APIs de casas de aposta (se disponível)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Pitch Deck — 10 Slides
|
||||||
|
|
||||||
|
### Slide 1 — Capa
|
||||||
|
**MIDAS** — IA para aposta responsável.
|
||||||
|
"O Waze das apostas esportivas: não te diz onde apostar, mas te impede de se perder."
|
||||||
|
|
||||||
|
### Slide 2 — Problema
|
||||||
|
- 40M+ de brasileiros apostam online
|
||||||
|
- 70% não têm controle de banca
|
||||||
|
- Lei 14.790 exige jogo responsável — operadoras não têm solução
|
||||||
|
- Apostador que quebra = churn para a casa de aposta
|
||||||
|
|
||||||
|
### Slide 3 — Solução
|
||||||
|
App com IA que analisa padrões, alerta riscos e ensina gestão de banca com gamificação. Software puro, sem regulação financeira.
|
||||||
|
|
||||||
|
### Slide 4 — Mercado
|
||||||
|
- TAM: R$ 12B (mercado de apostas BR regulado)
|
||||||
|
- SAM: R$ 1,2B (serviços de valor agregado para apostadores)
|
||||||
|
- SOM: R$ 60M (5% dos apostadores pagando R$12/mês em 3 anos)
|
||||||
|
|
||||||
|
### Slide 5 — Produto (Demo)
|
||||||
|
Screenshots do app: dashboard, alertas, gamificação, relatórios.
|
||||||
|
|
||||||
|
### Slide 6 — Modelo de Negócio
|
||||||
|
Dual revenue: B2C (freemium → premium R$19,90/mês) + B2B (SDK para casas de aposta, R$1,50-3,00/MAU).
|
||||||
|
|
||||||
|
### Slide 7 — Tração / Validação
|
||||||
|
Waitlist, beta users, métricas de engajamento, LOIs de operadoras (se houver).
|
||||||
|
|
||||||
|
### Slide 8 — Go-to-Market
|
||||||
|
TikTok viral + influencers de bet + referral. B2B: compliance Lei 14.790 como porta de entrada.
|
||||||
|
|
||||||
|
### Slide 9 — Financeiro
|
||||||
|
MRR projetado mês 12: R$ 200-400K. Breakeven mês 8-10. (Ver seção 8 abaixo.)
|
||||||
|
|
||||||
|
### Slide 10 — Ask
|
||||||
|
Captação: R$ 1,5M pre-seed.
|
||||||
|
Uso: 50% produto/tech, 30% marketing/aquisição, 20% operação.
|
||||||
|
Meta: 50k usuários + 2 contratos B2B em 12 meses.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Projeção Financeira — Meses 1-12
|
||||||
|
|
||||||
|
### Premissas
|
||||||
|
|
||||||
|
- Crescimento de usuários: 20% MoM orgânico + paid
|
||||||
|
- Conversão free→premium: 8% (mês 1) → 15% (mês 12)
|
||||||
|
- ARPU premium: R$ 19,90
|
||||||
|
- Churn: 12% (mês 1) → 8% (mês 12)
|
||||||
|
- 1 contrato B2B fecha no mês 6 (10k MAU a R$2,50/MAU)
|
||||||
|
- Custo mensal base: R$ 35.000 (team + infra)
|
||||||
|
|
||||||
|
### Projeção B2C
|
||||||
|
|
||||||
|
| Mês | Usuários totais | Pagantes | Conv. % | MRR B2C | Custo |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 1 | 1.000 | 80 | 8% | R$ 1.592 | R$ 35.000 |
|
||||||
|
| 2 | 1.800 | 162 | 9% | R$ 3.224 | R$ 37.000 |
|
||||||
|
| 3 | 3.000 | 300 | 10% | R$ 5.970 | R$ 40.000 |
|
||||||
|
| 4 | 4.500 | 495 | 11% | R$ 9.851 | R$ 42.000 |
|
||||||
|
| 5 | 6.500 | 780 | 12% | R$ 15.522 | R$ 45.000 |
|
||||||
|
| 6 | 9.000 | 1.170 | 13% | R$ 23.283 | R$ 50.000 |
|
||||||
|
| 7 | 12.000 | 1.680 | 14% | R$ 33.432 | R$ 55.000 |
|
||||||
|
| 8 | 15.500 | 2.170 | 14% | R$ 43.183 | R$ 58.000 |
|
||||||
|
| 9 | 19.500 | 2.925 | 15% | R$ 58.208 | R$ 62.000 |
|
||||||
|
| 10 | 24.000 | 3.600 | 15% | R$ 71.640 | R$ 65.000 |
|
||||||
|
| 11 | 29.000 | 4.350 | 15% | R$ 86.565 | R$ 68.000 |
|
||||||
|
| 12 | 35.000 | 5.250 | 15% | R$ 104.475 | R$ 72.000 |
|
||||||
|
|
||||||
|
### B2B (a partir do mês 6)
|
||||||
|
|
||||||
|
| Mês | MAU B2B | MRR B2B |
|
||||||
|
|---|---|---|
|
||||||
|
| 6 | 10.000 | R$ 25.000 |
|
||||||
|
| 7 | 12.000 | R$ 30.000 |
|
||||||
|
| 8 | 15.000 | R$ 37.500 |
|
||||||
|
| 9 | 20.000 | R$ 50.000 |
|
||||||
|
| 10 | 25.000 | R$ 62.500 |
|
||||||
|
| 11 | 30.000 | R$ 75.000 |
|
||||||
|
| 12 | 40.000 | R$ 100.000 |
|
||||||
|
|
||||||
|
### Consolidado
|
||||||
|
|
||||||
|
| Mês | MRR Total | Custo | Resultado |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | R$ 1.592 | R$ 35.000 | -R$ 33.408 |
|
||||||
|
| 3 | R$ 5.970 | R$ 40.000 | -R$ 34.030 |
|
||||||
|
| 6 | R$ 48.283 | R$ 50.000 | -R$ 1.717 |
|
||||||
|
| 9 | R$ 108.208 | R$ 62.000 | +R$ 46.208 |
|
||||||
|
| 12 | R$ 204.475 | R$ 72.000 | +R$ 132.475 |
|
||||||
|
|
||||||
|
**MRR mês 12:** ~R$ 204K
|
||||||
|
**ARR mês 12:** ~R$ 2,45M
|
||||||
|
**Breakeven:** Mês 6-7
|
||||||
|
**Burn total até breakeven:** ~R$ 200K
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resumo Executivo
|
||||||
|
|
||||||
|
O MIDAS é um SaaS com modelo dual B2C+B2B em um mercado de 40M+ apostadores brasileiros com vento regulatório a favor (Lei 14.790). Unit economics saudáveis (LTV/CAC 5-8x B2C), breakeven rápido com 1 contrato B2B, e potencial de ARR >R$2M no primeiro ano. Produto pode ser construído com time enxuto (3-4 pessoas) e investimento inicial de R$1,5M.
|
||||||
|
|
||||||
|
**Próximos passos imediatos:**
|
||||||
|
1. Validar demanda com landing page + waitlist (1 semana)
|
||||||
|
2. Iniciar MVP (4 semanas)
|
||||||
|
3. Recrutar 5 micro-influencers para beta (paralelo ao MVP)
|
||||||
|
4. Agendar reuniões com heads de compliance de 3 operadoras
|
||||||
1
frontend
Submodule
1
frontend
Submodule
Submodule frontend added at 696088ed3a
Reference in New Issue
Block a user