21e05148c8
Deploy Iddaai Backend / build-and-deploy (push) Failing after 3m56s
- Add LeagueTier DB model and Prisma schema - Add league-tiers service (CRUD, sync, retrain trigger) - Add league-tiers controller with admin API endpoints - Add /v1/admin/retrain endpoint in AI engine (extract→train→reload pipeline) - Retrain V25 Pro with 48 quality leagues (MS accuracy: 26.9%→51.4%) - Update qualified_leagues.json (443→48 leagues) - Include V25 model files in repo for Docker deployment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
893 lines
37 KiB
Plaintext
Executable File
893 lines
37 KiB
Plaintext
Executable File
generator client {
|
||
provider = "prisma-client-js"
|
||
binaryTargets = ["native", "linux-musl-arm64-openssl-3.0.x", "linux-musl-openssl-3.0.x"]
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
url = env("DATABASE_URL")
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────
|
||
// Domain Models — Sports Data
|
||
// ─────────────────────────────────────────────────────────────
|
||
|
||
model Country {
|
||
id String @id
|
||
name String @unique
|
||
flagUrl String? @map("flag_url")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
leagues League[]
|
||
|
||
@@map("countries")
|
||
}
|
||
|
||
model League {
|
||
id String @id
|
||
name String
|
||
countryId String? @map("country_id")
|
||
sport Sport
|
||
competitionSlug String? @map("competition_slug")
|
||
code String?
|
||
logoUrl String? @map("logo_url")
|
||
sortOrder Int? @map("sort_order")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
country Country? @relation(fields: [countryId], references: [id])
|
||
liveMatches LiveMatch[]
|
||
matches Match[]
|
||
leagueTier LeagueTier?
|
||
|
||
@@unique([name, countryId, sport])
|
||
@@index([sport])
|
||
@@index([countryId])
|
||
@@map("leagues")
|
||
}
|
||
|
||
model Team {
|
||
id String @id
|
||
name String
|
||
slug String?
|
||
sport Sport
|
||
logoUrl String? @map("logo_url")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
awayMatchesLive LiveMatch[] @relation("AwayTeamLive")
|
||
homeMatchesLive LiveMatch[] @relation("HomeTeamLive")
|
||
playerEvents MatchPlayerEvents[]
|
||
playerParticipations MatchPlayerParticipation[]
|
||
basketballPlayerStats BasketballPlayerStats[]
|
||
footballTeamStats FootballTeamStats[]
|
||
basketballTeamStats BasketballTeamStats[]
|
||
awayMatches Match[] @relation("AwayTeam")
|
||
homeMatches Match[] @relation("HomeTeam")
|
||
eloRating TeamEloRating?
|
||
|
||
@@index([name])
|
||
@@index([sport])
|
||
@@map("teams")
|
||
}
|
||
|
||
model Player {
|
||
id String @id
|
||
name String
|
||
slug String? @unique
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
assistEvents MatchPlayerEvents[] @relation("AssistPlayer")
|
||
playerEvents MatchPlayerEvents[] @relation("EventPlayer")
|
||
substitutedOutEvents MatchPlayerEvents[] @relation("SubstitutedOut")
|
||
participations MatchPlayerParticipation[]
|
||
playerStats BasketballPlayerStats[]
|
||
|
||
@@index([name])
|
||
@@map("players")
|
||
}
|
||
|
||
model Match {
|
||
id String @id
|
||
leagueId String? @map("league_id")
|
||
homeTeamId String? @map("home_team_id")
|
||
awayTeamId String? @map("away_team_id")
|
||
sport Sport
|
||
matchName String? @map("match_name")
|
||
matchSlug String? @map("match_slug")
|
||
mstUtc BigInt @map("mst_utc")
|
||
status String?
|
||
state String?
|
||
scoreHome Int? @map("score_home")
|
||
scoreAway Int? @map("score_away")
|
||
htScoreHome Int? @map("ht_score_home")
|
||
htScoreAway Int? @map("ht_score_away")
|
||
winner String?
|
||
iddaaCode String? @map("iddaa_code")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
footballAiFeatures FootballAiFeature?
|
||
basketballAiFeatures BasketballAiFeature?
|
||
officials MatchOfficial[]
|
||
playerEvents MatchPlayerEvents[]
|
||
playerParticipations MatchPlayerParticipation[]
|
||
basketballPlayerStats BasketballPlayerStats[]
|
||
footballTeamStats FootballTeamStats[]
|
||
basketballTeamStats BasketballTeamStats[]
|
||
awayTeam Team? @relation("AwayTeam", fields: [awayTeamId], references: [id])
|
||
homeTeam Team? @relation("HomeTeam", fields: [homeTeamId], references: [id])
|
||
league League? @relation(fields: [leagueId], references: [id])
|
||
oddCategories OddCategory[]
|
||
prediction Prediction?
|
||
predictionOutcomes PredictionOutcome[]
|
||
couponItems UserCouponItem[]
|
||
|
||
@@index([awayTeamId])
|
||
@@index([homeTeamId])
|
||
@@index([iddaaCode])
|
||
@@index([leagueId])
|
||
@@index([mstUtc(sort: Desc)])
|
||
@@index([sport])
|
||
@@index([state])
|
||
@@index([status, mstUtc(sort: Desc)])
|
||
@@map("matches")
|
||
}
|
||
|
||
model LiveMatch {
|
||
id String @id
|
||
leagueId String? @map("league_id")
|
||
homeTeamId String? @map("home_team_id")
|
||
awayTeamId String? @map("away_team_id")
|
||
sport String?
|
||
matchName String? @map("match_name")
|
||
matchSlug String? @map("match_slug")
|
||
mstUtc BigInt? @map("mst_utc")
|
||
status String?
|
||
state String?
|
||
substate String?
|
||
scoreHome Int? @map("score_home")
|
||
scoreAway Int? @map("score_away")
|
||
htScoreHome Int? @map("ht_score_home")
|
||
htScoreAway Int? @map("ht_score_away")
|
||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||
odds Json?
|
||
oddsUpdatedAt DateTime? @map("odds_updated_at")
|
||
refereeName String? @map("referee_name")
|
||
lineups Json?
|
||
sidelined Json?
|
||
awayTeam Team? @relation("AwayTeamLive", fields: [awayTeamId], references: [id])
|
||
homeTeam Team? @relation("HomeTeamLive", fields: [homeTeamId], references: [id])
|
||
league League? @relation(fields: [leagueId], references: [id])
|
||
|
||
@@index([mstUtc])
|
||
@@index([state])
|
||
@@map("live_matches")
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────
|
||
// Match Details — Stats, Events, Participation, Officials
|
||
// ─────────────────────────────────────────────────────────────
|
||
|
||
model FootballAiFeature {
|
||
matchId String @id @map("match_id")
|
||
homeElo Float @default(1500.0) @map("home_elo")
|
||
awayElo Float @default(1500.0) @map("away_elo")
|
||
homeHomeElo Float @default(1500.0) @map("home_home_elo")
|
||
awayAwayElo Float @default(1500.0) @map("away_away_elo")
|
||
homeFormElo Float @default(1500.0) @map("home_form_elo")
|
||
awayFormElo Float @default(1500.0) @map("away_form_elo")
|
||
eloDiff Float @default(0.0) @map("elo_diff")
|
||
homeFormScore Float @default(50.0) @map("home_form_score")
|
||
awayFormScore Float @default(50.0) @map("away_form_score")
|
||
homeGoalsAvg5 Float @default(0.0) @map("home_goals_avg_5")
|
||
awayGoalsAvg5 Float @default(0.0) @map("away_goals_avg_5")
|
||
homeConcededAvg5 Float @default(0.0) @map("home_conceded_avg_5")
|
||
awayConcededAvg5 Float @default(0.0) @map("away_conceded_avg_5")
|
||
homeCleanSheetRate Float @default(0.0) @map("home_clean_sheet_rate")
|
||
awayCleanSheetRate Float @default(0.0) @map("away_clean_sheet_rate")
|
||
homeScoringRate Float @default(0.0) @map("home_scoring_rate")
|
||
awayScoringRate Float @default(0.0) @map("away_scoring_rate")
|
||
homeWinStreak Int @default(0) @map("home_win_streak")
|
||
awayWinStreak Int @default(0) @map("away_win_streak")
|
||
impliedHome Float @default(0.33) @map("implied_home")
|
||
impliedDraw Float @default(0.33) @map("implied_draw")
|
||
impliedAway Float @default(0.33) @map("implied_away")
|
||
impliedOver25 Float @default(0.5) @map("implied_over25")
|
||
impliedBttsYes Float @default(0.5) @map("implied_btts_yes")
|
||
oddsOverround Float @default(0.0) @map("odds_overround")
|
||
homeAvgPossession Float @default(50.0) @map("home_avg_possession")
|
||
awayAvgPossession Float @default(50.0) @map("away_avg_possession")
|
||
homeAvgShotsOnTarget Float @default(0.0) @map("home_avg_shots_on_target")
|
||
awayAvgShotsOnTarget Float @default(0.0) @map("away_avg_shots_on_target")
|
||
homeShotConversion Float @default(0.0) @map("home_shot_conversion")
|
||
awayShotConversion Float @default(0.0) @map("away_shot_conversion")
|
||
homeAvgCorners Float @default(0.0) @map("home_avg_corners")
|
||
awayAvgCorners Float @default(0.0) @map("away_avg_corners")
|
||
h2hTotal Int @default(0) @map("h2h_total")
|
||
h2hHomeWinRate Float @default(0.0) @map("h2h_home_win_rate")
|
||
h2hAvgGoals Float @default(0.0) @map("h2h_avg_goals")
|
||
h2hOver25Rate Float @default(0.0) @map("h2h_over25_rate")
|
||
h2hBttsRate Float @default(0.0) @map("h2h_btts_rate")
|
||
refereeAvgCards Float @default(0.0) @map("referee_avg_cards")
|
||
refereeHomeBias Float @default(0.0) @map("referee_home_bias")
|
||
refereeAvgGoals Float @default(0.0) @map("referee_avg_goals")
|
||
leagueAvgGoals Float @default(0.0) @map("league_avg_goals")
|
||
leagueHomeWinPct Float @default(0.0) @map("league_home_win_pct")
|
||
leagueOver25Pct Float @default(0.0) @map("league_over25_pct")
|
||
missingPlayersImpact Float @default(0.0) @map("missing_players_impact")
|
||
oddsMovementHome Float? @map("odds_movement_home")
|
||
oddsMovementDraw Float? @map("odds_movement_draw")
|
||
oddsMovementAway Float? @map("odds_movement_away")
|
||
oddsMovementO25 Float? @map("odds_movement_o25")
|
||
oddsMovementBtts Float? @map("odds_movement_btts")
|
||
oddsSharpness Float? @map("odds_sharpness")
|
||
calculatorVer String @default("v2.0") @map("calculator_ver")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||
|
||
@@map("football_ai_features")
|
||
}
|
||
|
||
model BasketballAiFeature {
|
||
matchId String @id @map("match_id")
|
||
homeElo Float @default(1500.0) @map("home_elo")
|
||
awayElo Float @default(1500.0) @map("away_elo")
|
||
homeHomeElo Float @default(1500.0) @map("home_home_elo")
|
||
awayAwayElo Float @default(1500.0) @map("away_away_elo")
|
||
homeFormElo Float @default(1500.0) @map("home_form_elo")
|
||
awayFormElo Float @default(1500.0) @map("away_form_elo")
|
||
eloDiff Float @default(0.0) @map("elo_diff")
|
||
homeFormScore Float @default(50.0) @map("home_form_score")
|
||
awayFormScore Float @default(50.0) @map("away_form_score")
|
||
homePtsAvg5 Float @default(0.0) @map("home_pts_avg_5")
|
||
awayPtsAvg5 Float @default(0.0) @map("away_pts_avg_5")
|
||
homeConcededAvg5 Float @default(0.0) @map("home_conceded_avg_5")
|
||
awayConcededAvg5 Float @default(0.0) @map("away_conceded_avg_5")
|
||
homeWinStreak Int @default(0) @map("home_win_streak")
|
||
awayWinStreak Int @default(0) @map("away_win_streak")
|
||
impliedHome Float @default(0.5) @map("implied_home")
|
||
impliedAway Float @default(0.5) @map("implied_away")
|
||
impliedOverTotal Float @default(0.5) @map("implied_over_total")
|
||
impliedSpreadHome Float @default(0.5) @map("implied_spread_home")
|
||
oddsOverround Float @default(0.0) @map("odds_overround")
|
||
homeAvgPts Float @default(0.0) @map("home_avg_pts")
|
||
awayAvgPts Float @default(0.0) @map("away_avg_pts")
|
||
homeAvgRebounds Float @default(0.0) @map("home_avg_rebounds")
|
||
awayAvgRebounds Float @default(0.0) @map("away_avg_rebounds")
|
||
homeFgPct Float @default(0.0) @map("home_fg_pct")
|
||
awayFgPct Float @default(0.0) @map("away_fg_pct")
|
||
homeAvgThreePtMade Float @default(0.0) @map("home_avg_three_pt_made")
|
||
awayAvgThreePtMade Float @default(0.0) @map("away_avg_three_pt_made")
|
||
homeAvgTurnovers Float @default(0.0) @map("home_avg_turnovers")
|
||
awayAvgTurnovers Float @default(0.0) @map("away_avg_turnovers")
|
||
h2hTotal Int @default(0) @map("h2h_total")
|
||
h2hHomeWinRate Float @default(0.0) @map("h2h_home_win_rate")
|
||
h2hAvgPts Float @default(0.0) @map("h2h_avg_pts")
|
||
h2hAvgMargin Float @default(0.0) @map("h2h_avg_margin")
|
||
missingPlayersImpact Float @default(0.0) @map("missing_players_impact")
|
||
calculatorVer String @default("v2.0") @map("calculator_ver")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||
|
||
@@map("basketball_ai_features")
|
||
}
|
||
|
||
model TeamEloRating {
|
||
teamId String @id @map("team_id")
|
||
overallElo Float @default(1500.0) @map("overall_elo")
|
||
homeElo Float @default(1500.0) @map("home_elo")
|
||
awayElo Float @default(1500.0) @map("away_elo")
|
||
formElo Float @default(1500.0) @map("form_elo")
|
||
matchesPlayed Int @default(0) @map("matches_played")
|
||
recentForm String @default("") @map("recent_form")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||
|
||
@@map("team_elo_ratings")
|
||
}
|
||
|
||
model MatchPlayerEvents {
|
||
id Int @id @default(autoincrement())
|
||
matchId String @map("match_id")
|
||
playerId String @map("player_id")
|
||
teamId String @map("team_id")
|
||
eventType EventType @map("event_type")
|
||
eventSubtype String? @map("event_subtype")
|
||
timeMinute String @map("time_minute")
|
||
timeSeconds Int? @map("time_seconds")
|
||
periodId Int? @map("period_id")
|
||
assistPlayerId String? @map("assist_player_id")
|
||
scoreAfter String? @map("score_after")
|
||
playerOutId String? @map("player_out_id")
|
||
position MatchPosition?
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
assistPlayer Player? @relation("AssistPlayer", fields: [assistPlayerId], references: [id])
|
||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||
player Player @relation("EventPlayer", fields: [playerId], references: [id], onDelete: Cascade)
|
||
substitutedOut Player? @relation("SubstitutedOut", fields: [playerOutId], references: [id])
|
||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([assistPlayerId])
|
||
@@index([eventType])
|
||
@@index([matchId])
|
||
@@index([playerId])
|
||
@@index([teamId])
|
||
@@map("match_player_events")
|
||
}
|
||
|
||
model MatchPlayerParticipation {
|
||
id Int @id @default(autoincrement())
|
||
matchId String @map("match_id")
|
||
playerId String @map("player_id")
|
||
teamId String @map("team_id")
|
||
position PlayerPosition?
|
||
shirtNumber Int? @map("shirt_number")
|
||
isStarting Boolean @default(true) @map("is_starting")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||
player Player @relation(fields: [playerId], references: [id], onDelete: Cascade)
|
||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([matchId, playerId, teamId])
|
||
@@index([matchId])
|
||
@@index([playerId])
|
||
@@index([teamId])
|
||
@@map("match_player_participation")
|
||
}
|
||
|
||
model BasketballPlayerStats {
|
||
id Int @id @default(autoincrement())
|
||
matchId String @map("match_id")
|
||
playerId String @map("player_id")
|
||
teamId String @map("team_id")
|
||
minutes String?
|
||
points Int?
|
||
rebounds Int?
|
||
assists Int?
|
||
steals Int?
|
||
blocks Int?
|
||
turnovers Int?
|
||
fgMade Int? @map("fg_made")
|
||
fgAttempted Int? @map("fg_attempted")
|
||
threePtMade Int? @map("three_pt_made")
|
||
threePtAttempted Int? @map("three_pt_attempted")
|
||
ftMade Int? @map("ft_made")
|
||
ftAttempted Int? @map("ft_attempted")
|
||
fouls Int?
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||
player Player @relation(fields: [playerId], references: [id], onDelete: Cascade)
|
||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([matchId, playerId, teamId])
|
||
@@index([matchId])
|
||
@@map("basketball_player_stats")
|
||
}
|
||
|
||
model FootballTeamStats {
|
||
id Int @id @default(autoincrement())
|
||
matchId String @map("match_id")
|
||
teamId String @map("team_id")
|
||
possessionPercentage Float? @map("possession_percentage")
|
||
shotsOnTarget Int? @map("shots_on_target")
|
||
shotsOffTarget Int? @map("shots_off_target")
|
||
totalShots Int? @map("total_shots")
|
||
totalPasses Int? @map("total_passes")
|
||
corners Int?
|
||
fouls Int?
|
||
offsides Int?
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([matchId, teamId])
|
||
@@index([matchId])
|
||
@@index([teamId])
|
||
@@map("football_team_stats")
|
||
}
|
||
|
||
model BasketballTeamStats {
|
||
id Int @id @default(autoincrement())
|
||
matchId String @map("match_id")
|
||
teamId String @map("team_id")
|
||
points Int?
|
||
rebounds Int?
|
||
assists Int?
|
||
fgMade Int? @map("fg_made")
|
||
fgAttempted Int? @map("fg_attempted")
|
||
threePtMade Int? @map("three_pt_made")
|
||
threePtAttempted Int? @map("three_pt_attempted")
|
||
ftMade Int? @map("ft_made")
|
||
ftAttempted Int? @map("ft_attempted")
|
||
steals Int?
|
||
blocks Int?
|
||
turnovers Int?
|
||
fouls Int?
|
||
q1Score Int? @map("q1_score")
|
||
q2Score Int? @map("q2_score")
|
||
q3Score Int? @map("q3_score")
|
||
q4Score Int? @map("q4_score")
|
||
otScore Int? @map("ot_score")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([matchId, teamId])
|
||
@@index([matchId])
|
||
@@index([teamId])
|
||
@@map("basketball_team_stats")
|
||
}
|
||
|
||
model MatchOfficial {
|
||
id Int @id @default(autoincrement())
|
||
matchId String @map("match_id")
|
||
name String
|
||
roleId Int @map("role_id")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||
role OfficialRole @relation(fields: [roleId], references: [id])
|
||
|
||
@@unique([matchId, name, roleId])
|
||
@@index([matchId])
|
||
@@index([roleId])
|
||
@@map("match_officials")
|
||
}
|
||
|
||
model OfficialRole {
|
||
id Int @id @default(autoincrement())
|
||
name String @unique
|
||
officials MatchOfficial[]
|
||
|
||
@@map("official_roles")
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────
|
||
// Odds & Predictions
|
||
// ─────────────────────────────────────────────────────────────
|
||
|
||
model OddCategory {
|
||
dbId Int @id @default(autoincrement()) @map("db_id")
|
||
matchId String @map("match_id")
|
||
categoryJsonId Int? @map("category_json_id")
|
||
name String?
|
||
sport Sport?
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||
selections OddSelection[]
|
||
|
||
@@unique([matchId, name])
|
||
@@index([matchId])
|
||
@@index([sport])
|
||
@@map("odd_categories")
|
||
}
|
||
|
||
model OddSelection {
|
||
dbId Int @id @default(autoincrement()) @map("db_id")
|
||
categoryId Int @map("odd_category_db_id")
|
||
name String?
|
||
oddValue String? @map("odd_value")
|
||
openingValue String? @map("opening_value")
|
||
position String?
|
||
sov Float?
|
||
state String?
|
||
sport Sport?
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @default(now()) @map("updated_at")
|
||
category OddCategory @relation(fields: [categoryId], references: [dbId], onDelete: Cascade)
|
||
history OddsHistory[]
|
||
|
||
@@unique([categoryId, name])
|
||
@@index([categoryId])
|
||
@@index([sport])
|
||
@@map("odd_selections")
|
||
}
|
||
|
||
model OddsHistory {
|
||
id BigInt @id @default(autoincrement())
|
||
selectionId Int @map("selection_id")
|
||
matchId String @map("match_id")
|
||
previousValue Float @map("previous_value")
|
||
newValue Float @map("new_value")
|
||
bookmaker String? @default("MACKOLIK")
|
||
changeTime DateTime @default(now()) @map("change_time")
|
||
selection OddSelection @relation(fields: [selectionId], references: [dbId], onDelete: Cascade)
|
||
|
||
@@index([matchId, changeTime])
|
||
@@index([selectionId])
|
||
@@map("odds_history")
|
||
}
|
||
|
||
model Prediction {
|
||
matchId String @id @map("match_id")
|
||
predictionJson Json @map("prediction_json")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||
|
||
@@map("predictions")
|
||
}
|
||
|
||
model PredictionRun {
|
||
id BigInt @id @default(autoincrement())
|
||
matchId String @map("match_id")
|
||
engineVersion String @map("engine_version")
|
||
decisionTraceId String? @map("decision_trace_id")
|
||
generatedAt DateTime @default(now()) @map("generated_at")
|
||
oddsSnapshot Json? @map("odds_snapshot")
|
||
payloadSummary Json @map("payload_summary")
|
||
eventualOutcome String? @map("eventual_outcome")
|
||
unitProfit Float? @map("unit_profit")
|
||
|
||
@@index([matchId, generatedAt(sort: Desc)])
|
||
@@index([engineVersion, generatedAt(sort: Desc)])
|
||
@@map("prediction_runs")
|
||
}
|
||
|
||
model PredictionOutcome {
|
||
id BigInt @id @default(autoincrement())
|
||
matchId String @map("match_id")
|
||
market String
|
||
pick String
|
||
probability Float
|
||
confidence Float
|
||
odds Float?
|
||
playable Boolean @default(false)
|
||
engineVersion String @map("engine_version")
|
||
generatedAt DateTime @default(now()) @map("generated_at")
|
||
resolved Boolean @default(false)
|
||
hit Boolean?
|
||
resolvedAt DateTime? @map("resolved_at")
|
||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([matchId, market, engineVersion])
|
||
@@index([market, resolvedAt(sort: Desc)])
|
||
@@index([resolved, generatedAt])
|
||
@@index([playable, resolved])
|
||
@@map("prediction_outcomes")
|
||
}
|
||
|
||
model AiPredictionsLog {
|
||
id Int @id @default(autoincrement())
|
||
matchId String @map("match_id")
|
||
modelVersion String @map("model_version")
|
||
recommendedBets Json? @map("recommended_bets")
|
||
confidenceScore Float? @map("confidence_score")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
isResolved Boolean @default(false) @map("is_resolved")
|
||
actualResult String? @map("actual_result")
|
||
isCorrect Boolean? @map("is_correct")
|
||
accuracyScore Float? @map("accuracy_score")
|
||
|
||
@@index([matchId])
|
||
@@index([createdAt(sort: Desc)])
|
||
@@map("ai_predictions_log")
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────
|
||
// User Domain — Auth, Coupons, Usage
|
||
// ─────────────────────────────────────────────────────────────
|
||
|
||
model User {
|
||
id String @id @default(uuid())
|
||
email String @unique
|
||
passwordHash String @map("password_hash")
|
||
firstName String? @map("first_name")
|
||
lastName String? @map("last_name")
|
||
role UserRole @default(user)
|
||
subscriptionStatus SubscriptionStatus @default(free) @map("subscription_status")
|
||
subscriptionExpiresAt DateTime? @map("subscription_expires_at")
|
||
encryptedApiKey String? @map("encrypted_api_key")
|
||
isActive Boolean @default(true) @map("is_active")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
deletedAt DateTime? @map("deleted_at")
|
||
analyses Analysis[]
|
||
refreshTokens RefreshToken[]
|
||
usageLimit UsageLimit?
|
||
subscription Subscription?
|
||
coupons UserCoupon[]
|
||
totoCoupons TotoCoupon[]
|
||
|
||
@@index([email])
|
||
@@index([subscriptionStatus, subscriptionExpiresAt])
|
||
@@map("users")
|
||
}
|
||
|
||
model Subscription {
|
||
id String @id @default(uuid())
|
||
userId String @unique @map("user_id")
|
||
paddleSubscriptionId String? @unique @map("paddle_subscription_id")
|
||
paddleCustomerId String? @map("paddle_customer_id")
|
||
plan SubscriptionStatus @default(free)
|
||
billingInterval BillingInterval? @map("billing_interval")
|
||
currentPeriodStart DateTime? @map("current_period_start")
|
||
currentPeriodEnd DateTime? @map("current_period_end")
|
||
cancelledAt DateTime? @map("cancelled_at")
|
||
cancelEffectiveDate DateTime? @map("cancel_effective_date")
|
||
paddlePriceId String? @map("paddle_price_id")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([paddleSubscriptionId])
|
||
@@index([paddleCustomerId])
|
||
@@map("subscriptions")
|
||
}
|
||
|
||
model RefreshToken {
|
||
id String @id @default(uuid())
|
||
token String @unique
|
||
userId String @map("user_id")
|
||
expiresAt DateTime @map("expires_at")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([token])
|
||
@@index([userId])
|
||
@@map("refresh_tokens")
|
||
}
|
||
|
||
model UsageLimit {
|
||
id Int @id @default(autoincrement())
|
||
userId String @unique @map("user_id")
|
||
analysisCount Int @default(0) @map("analysis_count")
|
||
couponCount Int @default(0) @map("coupon_count")
|
||
maxAnalyses Int @default(3) @map("max_analyses")
|
||
maxCoupons Int @default(1) @map("max_coupons")
|
||
lastResetDate DateTime @map("last_reset_date") @db.Date
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([userId])
|
||
@@index([lastResetDate])
|
||
@@map("usage_limits")
|
||
}
|
||
|
||
model Analysis {
|
||
id Int @id @default(autoincrement())
|
||
userId String @map("user_id")
|
||
matchIds String @map("match_ids")
|
||
analysisResultJson String @map("analysis_result_json")
|
||
isDeleted Boolean @default(false) @map("is_deleted")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([userId])
|
||
@@index([createdAt(sort: Desc)])
|
||
@@map("analyses")
|
||
}
|
||
|
||
model UserCoupon {
|
||
id String @id @default(uuid())
|
||
userId String @map("user_id")
|
||
strategy String
|
||
totalOdds Float @map("total_odds")
|
||
status String @default("PENDING")
|
||
isPublic Boolean @default(false)
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
couponItems UserCouponItem[]
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([userId])
|
||
@@index([status])
|
||
@@map("user_coupons")
|
||
}
|
||
|
||
model UserCouponItem {
|
||
id Int @id @default(autoincrement())
|
||
couponId String @map("coupon_id")
|
||
matchId String @map("match_id")
|
||
selection String
|
||
oddAtTime Float @map("odd_at_time")
|
||
isCorrect Boolean? @map("is_correct")
|
||
coupon UserCoupon @relation(fields: [couponId], references: [id], onDelete: Cascade)
|
||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([couponId])
|
||
@@map("user_coupon_items")
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────
|
||
// Spor Toto Domain
|
||
// ─────────────────────────────────────────────────────────────
|
||
|
||
model TotoBulletin {
|
||
id String @id @default(uuid())
|
||
gameCycleNo Int @unique @map("game_cycle_no")
|
||
programName String? @map("program_name")
|
||
season String?
|
||
status TotoBulletinStatus @default(UPCOMING)
|
||
payinBeginDate DateTime? @map("payin_begin_date")
|
||
payinEndDate DateTime? @map("payin_end_date")
|
||
poolTotal Float? @map("pool_total")
|
||
rolloverAmount Float? @map("rollover_amount")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
matches TotoBulletinMatch[]
|
||
result TotoResult?
|
||
coupons TotoCoupon[]
|
||
|
||
@@index([status])
|
||
@@map("toto_bulletins")
|
||
}
|
||
|
||
model TotoBulletinMatch {
|
||
id Int @id @default(autoincrement())
|
||
bulletinId String @map("bulletin_id")
|
||
matchOrder Int @map("match_order")
|
||
homeTeamName String @map("home_team_name")
|
||
awayTeamName String @map("away_team_name")
|
||
leagueName String? @map("league_name")
|
||
kickoffTime DateTime? @map("kickoff_time")
|
||
matchId String? @map("match_id")
|
||
result TotoMatchResult?
|
||
isCancelled Boolean @default(false) @map("is_cancelled")
|
||
drawResult TotoMatchResult? @map("draw_result")
|
||
bulletin TotoBulletin @relation(fields: [bulletinId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([bulletinId, matchOrder])
|
||
@@index([bulletinId])
|
||
@@index([matchId])
|
||
@@map("toto_bulletin_matches")
|
||
}
|
||
|
||
model TotoResult {
|
||
id String @id @default(uuid())
|
||
bulletinId String @unique @map("bulletin_id")
|
||
winners15 Int @default(0) @map("winners_15")
|
||
prize15 Float? @map("prize_15")
|
||
winners14 Int @default(0) @map("winners_14")
|
||
prize14 Float? @map("prize_14")
|
||
winners13 Int @default(0) @map("winners_13")
|
||
prize13 Float? @map("prize_13")
|
||
winners12 Int @default(0) @map("winners_12")
|
||
prize12 Float? @map("prize_12")
|
||
rolloverNext Float? @map("rollover_next")
|
||
poolDistributed Float? @map("pool_distributed")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
bulletin TotoBulletin @relation(fields: [bulletinId], references: [id], onDelete: Cascade)
|
||
|
||
@@map("toto_results")
|
||
}
|
||
|
||
model TotoCoupon {
|
||
id String @id @default(uuid())
|
||
userId String @map("user_id")
|
||
bulletinId String @map("bulletin_id")
|
||
strategy String?
|
||
columnCount Int @map("column_count")
|
||
totalCost Float @map("total_cost")
|
||
status String @default("PENDING")
|
||
totalPrize Float? @map("total_prize")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
columns TotoColumn[]
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
bulletin TotoBulletin @relation(fields: [bulletinId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([userId])
|
||
@@index([bulletinId])
|
||
@@index([status])
|
||
@@map("toto_coupons")
|
||
}
|
||
|
||
model TotoColumn {
|
||
id Int @id @default(autoincrement())
|
||
couponId String @map("coupon_id")
|
||
predictions String @db.VarChar(15)
|
||
correctCount Int? @map("correct_count")
|
||
prizeAmount Float? @map("prize_amount")
|
||
coupon TotoCoupon @relation(fields: [couponId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([couponId])
|
||
@@index([correctCount])
|
||
@@map("toto_columns")
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────
|
||
// System & i18n
|
||
// ─────────────────────────────────────────────────────────────
|
||
|
||
model AppSetting {
|
||
key String @id
|
||
value String?
|
||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||
|
||
@@map("app_settings")
|
||
}
|
||
|
||
model Translation {
|
||
id String @id @default(uuid())
|
||
key String
|
||
locale String
|
||
value String
|
||
namespace String @default("common")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
@@unique([key, locale, namespace])
|
||
@@index([key])
|
||
@@index([locale])
|
||
@@index([namespace])
|
||
@@map("translations")
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────
|
||
// League Tier System — Controls which leagues are used for
|
||
// model training, live data fetching, and predictions.
|
||
// Managed from admin panel. When a league is added/removed,
|
||
// qualified_leagues.json is auto-regenerated and model
|
||
// retraining is triggered.
|
||
// ─────────────────────────────────────────────────────────────
|
||
|
||
model LeagueTier {
|
||
id Int @id @default(autoincrement())
|
||
leagueId String @map("league_id")
|
||
tier Int @default(1) // 1=Elmas, 2=Altın, 3=Gümüş
|
||
isActive Boolean @default(true) @map("is_active")
|
||
addedBy String? @map("added_by") // admin user id
|
||
notes String? // e.g. "Margin 0.138, 828 maç"
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
league League @relation(fields: [leagueId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([leagueId])
|
||
@@index([tier, isActive])
|
||
@@map("league_tiers")
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────
|
||
// Enums
|
||
// ─────────────────────────────────────────────────────────────
|
||
|
||
enum Sport {
|
||
football
|
||
basketball
|
||
}
|
||
|
||
enum UserRole {
|
||
user
|
||
superadmin
|
||
}
|
||
|
||
enum SubscriptionStatus {
|
||
free
|
||
plus
|
||
premium
|
||
past_due
|
||
cancelled
|
||
}
|
||
|
||
enum BillingInterval {
|
||
monthly
|
||
yearly
|
||
}
|
||
|
||
enum PlayerPosition {
|
||
goalkeeper
|
||
defender
|
||
midfielder
|
||
striker
|
||
}
|
||
|
||
enum EventType {
|
||
goal
|
||
card
|
||
substitute
|
||
}
|
||
|
||
enum MatchPosition {
|
||
home
|
||
away
|
||
}
|
||
|
||
enum TotoBulletinStatus {
|
||
UPCOMING
|
||
ACTIVE
|
||
COMPLETED
|
||
CANCELLED
|
||
}
|
||
|
||
enum TotoMatchResult {
|
||
HOME_WIN
|
||
DRAW
|
||
AWAY_WIN
|
||
}
|