commit 954ebccdd6e123ddb8dc28535450379155ed10ff Author: bigtux Date: Tue Feb 10 18:52:23 2026 -0300 Initial commit - MIDAS App educação financeira para apostadores (FastAPI + Next.js) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f83e305 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +.next/ +__pycache__/ +*.pyc +venv/ +.env +dist/ +.env.* +*.egg-info/ +.venv/ diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..3e5eac9 --- /dev/null +++ b/backend/app/config.py @@ -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() diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..0f0f61f --- /dev/null +++ b/backend/app/database.py @@ -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) diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..6f417a8 --- /dev/null +++ b/backend/app/main.py @@ -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"} diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/models/achievement.py b/backend/app/models/achievement.py new file mode 100644 index 0000000..f618153 --- /dev/null +++ b/backend/app/models/achievement.py @@ -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") diff --git a/backend/app/models/alert.py b/backend/app/models/alert.py new file mode 100644 index 0000000..aea6048 --- /dev/null +++ b/backend/app/models/alert.py @@ -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") diff --git a/backend/app/models/bankroll.py b/backend/app/models/bankroll.py new file mode 100644 index 0000000..468aa69 --- /dev/null +++ b/backend/app/models/bankroll.py @@ -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") diff --git a/backend/app/models/bet.py b/backend/app/models/bet.py new file mode 100644 index 0000000..34c85bf --- /dev/null +++ b/backend/app/models/bet.py @@ -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") diff --git a/backend/app/models/lesson.py b/backend/app/models/lesson.py new file mode 100644 index 0000000..542130d --- /dev/null +++ b/backend/app/models/lesson.py @@ -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") diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 0000000..c924abd --- /dev/null +++ b/backend/app/models/user.py @@ -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") diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/routers/achievements.py b/backend/app/routers/achievements.py new file mode 100644 index 0000000..5e12300 --- /dev/null +++ b/backend/app/routers/achievements.py @@ -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 + ] diff --git a/backend/app/routers/alerts.py b/backend/app/routers/alerts.py new file mode 100644 index 0000000..5932d93 --- /dev/null +++ b/backend/app/routers/alerts.py @@ -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 + ] + } diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py new file mode 100644 index 0000000..f6b5f1d --- /dev/null +++ b/backend/app/routers/auth.py @@ -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} diff --git a/backend/app/routers/bankroll.py b/backend/app/routers/bankroll.py new file mode 100644 index 0000000..8d30688 --- /dev/null +++ b/backend/app/routers/bankroll.py @@ -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} diff --git a/backend/app/routers/bets.py b/backend/app/routers/bets.py new file mode 100644 index 0000000..19f0da6 --- /dev/null +++ b/backend/app/routers/bets.py @@ -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 + } diff --git a/backend/app/routers/dashboard.py b/backend/app/routers/dashboard.py new file mode 100644 index 0000000..80bd519 --- /dev/null +++ b/backend/app/routers/dashboard.py @@ -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 + } diff --git a/backend/app/routers/lessons.py b/backend/app/routers/lessons.py new file mode 100644 index 0000000..6529c72 --- /dev/null +++ b/backend/app/routers/lessons.py @@ -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} diff --git a/backend/app/routers/reports.py b/backend/app/routers/reports.py new file mode 100644 index 0000000..d02730c --- /dev/null +++ b/backend/app/routers/reports.py @@ -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) diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/services/gamification.py b/backend/app/services/gamification.py new file mode 100644 index 0000000..8c60837 --- /dev/null +++ b/backend/app/services/gamification.py @@ -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 diff --git a/backend/app/services/risk_engine.py b/backend/app/services/risk_engine.py new file mode 100644 index 0000000..69e0be9 --- /dev/null +++ b/backend/app/services/risk_engine.py @@ -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} diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/utils/auth.py b/backend/app/utils/auth.py new file mode 100644 index 0000000..c392ff2 --- /dev/null +++ b/backend/app/utils/auth.py @@ -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 diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..ceb16aa --- /dev/null +++ b/backend/requirements.txt @@ -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 diff --git a/backend/seed_data.py b/backend/seed_data.py new file mode 100644 index 0000000..8f9261c --- /dev/null +++ b/backend/seed_data.py @@ -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()) diff --git a/docs/ARQUITETURA-TECNICA.md b/docs/ARQUITETURA-TECNICA.md new file mode 100644 index 0000000..65bc8c6 --- /dev/null +++ b/docs/ARQUITETURA-TECNICA.md @@ -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* diff --git a/docs/ESTRATEGIA-NEGOCIO.md b/docs/ESTRATEGIA-NEGOCIO.md new file mode 100644 index 0000000..53b24ff --- /dev/null +++ b/docs/ESTRATEGIA-NEGOCIO.md @@ -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 diff --git a/frontend b/frontend new file mode 160000 index 0000000..696088e --- /dev/null +++ b/frontend @@ -0,0 +1 @@ +Subproject commit 696088ed3aa872d94f70327f2e841bb5427bc243