Commit 90a1bdcf by Augusto

new data and endpoint fix

parent 383db895
......@@ -3,6 +3,7 @@ module ScoutingSystemScoreData
go 1.25.0
require (
github.com/gin-contrib/cors v1.7.0
github.com/gin-gonic/gin v1.11.0
github.com/joho/godotenv v1.5.1
github.com/matoous/go-nanoid/v2 v2.1.0
......
......@@ -16,6 +16,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/cors v1.7.0 h1:wZX2wuZ0o7rV2/1i7gb4Jn+gW7HBqaP91fizJkBUJOA=
github.com/gin-contrib/cors v1.7.0/go.mod h1:cI+h6iOAyxKRtUtC6iF/Si1KSFvGm/gK+kshxlCi8ro=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
......
package handlers
import (
"context"
"fmt"
"net/http"
"strconv"
......@@ -14,12 +15,13 @@ import (
)
type CoachHandler struct {
DB *gorm.DB
Service services.CoachService
}
func RegisterCoachRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewCoachService(db)
h := &CoachHandler{Service: service}
h := &CoachHandler{DB: db, Service: service}
coaches := rg.Group("/coaches")
coaches.GET("", h.List)
......@@ -32,6 +34,10 @@ type StructuredCoach struct {
ID string `json:"id"`
WyID *int `json:"wyId"`
TsID string `json:"tsId"`
CountryTsID *string `json:"countryTsId"`
CountryName *string `json:"countryName"`
TeamTsID *string `json:"teamTsId"`
TeamName *string `json:"teamName"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
MiddleName *string `json:"middleName"`
......@@ -57,35 +63,140 @@ type StructuredCoach struct {
DeletedAt *time.Time `json:"deletedAt"`
}
func toStructuredCoach(c models.Coach) StructuredCoach {
return StructuredCoach{
ID: c.ID,
WyID: nilIfZero(c.WyID),
TsID: c.TsID,
FirstName: c.FirstName,
LastName: c.LastName,
MiddleName: c.MiddleName,
ShortName: c.ShortName,
DateOfBirth: c.DateOfBirth,
NationalityWyID: c.NationalityWyID,
CurrentTeamWyID: c.CurrentTeamWyID,
Position: c.Position,
CoachingLicense: c.CoachingLicense,
YearsExperience: c.YearsExperience,
PreferredFormation: c.PreferredFormation,
JoinedAt: c.JoinedAt,
ContractUntil: c.ContractUntil,
UID: c.UID,
Deathday: c.Deathday,
Status: c.Status,
ImageDataURL: c.ImageDataURL,
APILastSyncedAt: c.APILastSyncedAt,
APISyncStatus: c.APISyncStatus,
IsActive: c.IsActive,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
DeletedAt: c.DeletedAt,
type CoachListResponse struct {
Data []StructuredCoach `json:"data"`
Meta map[string]interface{} `json:"meta"`
}
type CoachResponse struct {
Data StructuredCoach `json:"data"`
Meta map[string]interface{} `json:"meta"`
}
func (h *CoachHandler) enrichCoaches(ctx context.Context, coaches []models.Coach) ([]StructuredCoach, error) {
areaTsIDs := make([]string, 0, len(coaches))
teamTsIDs := make([]string, 0, len(coaches))
seenArea := map[string]struct{}{}
seenTeam := map[string]struct{}{}
for _, c := range coaches {
if c.CountryTsID != nil && *c.CountryTsID != "" {
if _, ok := seenArea[*c.CountryTsID]; !ok {
seenArea[*c.CountryTsID] = struct{}{}
areaTsIDs = append(areaTsIDs, *c.CountryTsID)
}
}
if c.TeamTsID != nil && *c.TeamTsID != "" {
if _, ok := seenTeam[*c.TeamTsID]; !ok {
seenTeam[*c.TeamTsID] = struct{}{}
teamTsIDs = append(teamTsIDs, *c.TeamTsID)
}
}
}
areaNameByTsID := map[string]string{}
if len(areaTsIDs) > 0 {
var areas []struct {
TsID string `gorm:"column:ts_id"`
Name string `gorm:"column:name"`
}
if err := h.DB.WithContext(ctx).
Model(&models.Area{}).
Select("ts_id", "name").
Where("ts_id IN ?", areaTsIDs).
Find(&areas).Error; err != nil {
return nil, err
}
for _, a := range areas {
if a.TsID != "" {
areaNameByTsID[a.TsID] = a.Name
}
}
}
teamNameByTsID := map[string]string{}
if len(teamTsIDs) > 0 {
var teams []struct {
TsID string `gorm:"column:ts_id"`
Name string `gorm:"column:name"`
}
if err := h.DB.WithContext(ctx).
Model(&models.Team{}).
Select("ts_id", "name").
Where("ts_id IN ?", teamTsIDs).
Find(&teams).Error; err != nil {
return nil, err
}
for _, t := range teams {
if t.TsID != "" {
teamNameByTsID[t.TsID] = t.Name
}
}
}
out := make([]StructuredCoach, 0, len(coaches))
for _, c := range coaches {
var countryName *string
if c.CountryTsID != nil {
if n, ok := areaNameByTsID[*c.CountryTsID]; ok {
nn := n
countryName = &nn
}
}
var teamName *string
if c.TeamTsID != nil {
if n, ok := teamNameByTsID[*c.TeamTsID]; ok {
nn := n
teamName = &nn
}
}
out = append(out, StructuredCoach{
ID: c.ID,
WyID: nilIfZero(c.WyID),
TsID: c.TsID,
CountryTsID: c.CountryTsID,
CountryName: countryName,
TeamTsID: c.TeamTsID,
TeamName: teamName,
FirstName: c.FirstName,
LastName: c.LastName,
MiddleName: c.MiddleName,
ShortName: c.ShortName,
DateOfBirth: c.DateOfBirth,
NationalityWyID: c.NationalityWyID,
CurrentTeamWyID: c.CurrentTeamWyID,
Position: c.Position,
CoachingLicense: c.CoachingLicense,
YearsExperience: c.YearsExperience,
PreferredFormation: c.PreferredFormation,
JoinedAt: c.JoinedAt,
ContractUntil: c.ContractUntil,
UID: c.UID,
Deathday: c.Deathday,
Status: c.Status,
ImageDataURL: c.ImageDataURL,
APILastSyncedAt: c.APILastSyncedAt,
APISyncStatus: c.APISyncStatus,
IsActive: c.IsActive,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
DeletedAt: c.DeletedAt,
})
}
return out, nil
}
func (h *CoachHandler) toStructuredCoach(ctx context.Context, c models.Coach) (StructuredCoach, error) {
rows, err := h.enrichCoaches(ctx, []models.Coach{c})
if err != nil {
return StructuredCoach{}, err
}
if len(rows) == 0 {
return StructuredCoach{}, nil
}
return rows[0], nil
}
func nilIfZero(v *int) *int {
......@@ -108,7 +219,7 @@ func nilIfZero(v *int) *int {
// @Param teamId query string false "Filter coaches by current team ID (wy_id)"
// @Param position query string false "Filter coaches by position (head_coach, assistant_coach, etc.)"
// @Param active query bool false "Filter active coaches only"
// @Success 200 {object} map[string]interface{}
// @Success 200 {object} handlers.CoachListResponse
// @Failure 500 {object} map[string]string
// @Router /coaches [get]
func (h *CoachHandler) List(c *gin.Context) {
......@@ -154,9 +265,10 @@ func (h *CoachHandler) List(c *gin.Context) {
return
}
structured := make([]StructuredCoach, 0, len(coaches))
for _, coach := range coaches {
structured = append(structured, toStructuredCoach(coach))
structured, err := h.enrichCoaches(c.Request.Context(), coaches)
if err != nil {
respondError(c, err)
return
}
page := offset/limit + 1
......@@ -182,7 +294,7 @@ func (h *CoachHandler) List(c *gin.Context) {
// @Description Returns a single coach by its internal ID.
// @Tags Coaches
// @Param id path string true "Coach internal identifier"
// @Success 200 {object} map[string]interface{}
// @Success 200 {object} handlers.CoachResponse
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /coaches/{id} [get]
......@@ -195,7 +307,11 @@ func (h *CoachHandler) GetByID(c *gin.Context) {
return
}
structured := toStructuredCoach(coach)
structured, err := h.toStructuredCoach(c.Request.Context(), coach)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/coaches/%s", id)
......@@ -214,7 +330,7 @@ func (h *CoachHandler) GetByID(c *gin.Context) {
// @Description Returns a single coach by its provider identifier: numeric values are treated as Wyscout wy_id, non-numeric as TheSports ts_id.
// @Tags Coaches
// @Param providerId path string true "Provider identifier (wy_id or ts_id)"
// @Success 200 {object} map[string]interface{}
// @Success 200 {object} handlers.CoachResponse
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /coaches/provider/{providerId} [get]
......@@ -227,7 +343,11 @@ func (h *CoachHandler) GetByProviderID(c *gin.Context) {
return
}
structured := toStructuredCoach(coach)
structured, err := h.toStructuredCoach(c.Request.Context(), coach)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/coaches/provider/%s", providerID)
......@@ -246,7 +366,7 @@ func (h *CoachHandler) GetByProviderID(c *gin.Context) {
// @Description Returns a single coach by its Wyscout wy_id identifier.
// @Tags Coaches
// @Param wyId path int true "Wyscout wy_id identifier"
// @Success 200 {object} map[string]interface{}
// @Success 200 {object} handlers.CoachResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
......@@ -265,7 +385,11 @@ func (h *CoachHandler) GetByWyID(c *gin.Context) {
return
}
structured := toStructuredCoach(coach)
structured, err := h.toStructuredCoach(c.Request.Context(), coach)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/coaches/wyscout/%d", wyID)
......
package handlers
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/services"
)
type CompetitionHandler struct {
Service services.CompetitionService
}
func RegisterCompetitionRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewCompetitionService(db)
h := &CompetitionHandler{Service: service}
competitions := rg.Group("/competitions")
competitions.GET("", h.List)
competitions.GET("/wyscout/:wyId", h.GetByWyID)
competitions.GET("/provider/:providerId", h.GetByProviderID)
competitions.GET("/:id", h.GetByID)
}
// List competitions
// @Summary List competitions
// @Description Returns a list of competitions with optional pagination.
// @Tags Competitions
// @Param limit query int false "Maximum number of items to return (default 100)"
// @Param offset query int false "Number of items to skip before starting to collect the result set (default 0)"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Router /competitions [get]
func (h *CompetitionHandler) List(c *gin.Context) {
limitStr := c.DefaultQuery("limit", "100")
offsetStr := c.DefaultQuery("offset", "0")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
limit = 100
}
offset, err := strconv.Atoi(offsetStr)
if err != nil || offset < 0 {
offset = 0
}
competitions, total, err := h.Service.ListCompetitions(c.Request.Context(), limit, offset)
if err != nil {
respondError(c, err)
return
}
page := offset/limit + 1
hasMore := int64(offset+limit) < total
timestamp := time.Now().UTC().Format(time.RFC3339)
c.JSON(http.StatusOK, gin.H{
"data": competitions,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": "/competitions",
"method": "GET",
"totalItems": total,
"page": page,
"limit": limit,
"hasMore": hasMore,
},
})
}
// GetByID returns a single competition by internal ID
// @Summary Get competition by ID
// @Description Returns a single competition by its internal ID.
// @Tags Competitions
// @Param id path string true "Competition internal identifier"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /competitions/{id} [get]
func (h *CompetitionHandler) GetByID(c *gin.Context) {
id := c.Param("id")
comp, err := h.Service.GetByID(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/competitions/%s", id)
c.JSON(http.StatusOK, gin.H{
"data": comp,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByProviderID returns a single competition by wy_id (numeric) or ts_id (string)
func (h *CompetitionHandler) GetByProviderID(c *gin.Context) {
providerID := c.Param("providerId")
comp, err := h.Service.GetByAnyProviderID(c.Request.Context(), providerID)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/competitions/provider/%s", providerID)
c.JSON(http.StatusOK, gin.H{
"data": comp,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByWyID returns a single competition by WyScout WyID
// @Summary Get competition by Wyscout ID
// @Description Returns a single competition by its Wyscout wy_id identifier.
// @Tags Competitions
// @Param wyId path int true "Wyscout wy_id identifier"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /competitions/wyscout/{wyId} [get]
func (h *CompetitionHandler) GetByWyID(c *gin.Context) {
wyIDStr := c.Param("wyId")
wyID, err := strconv.Atoi(wyIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid wyId"})
return
}
comp, err := h.Service.GetByWyID(c.Request.Context(), wyID)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/competitions/wyscout/%d", wyID)
c.JSON(http.StatusOK, gin.H{
"data": comp,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
......@@ -1776,7 +1776,13 @@ func (h *ImportHandler) ImportCoaches(c *gin.Context) {
for _, r := range payload.Results {
var coach models.Coach
if err := h.DB.Where("ts_id = ?", r.ID).First(&coach).Error; err != nil {
var err error
if r.UID != nil && *r.UID != "" {
err = h.DB.Where("uid = ?", *r.UID).First(&coach).Error
} else {
err = h.DB.Where("ts_id = ?", r.ID).First(&coach).Error
}
if err != nil {
if err == gorm.ErrRecordNotFound {
coach = models.Coach{
TsID: r.ID,
......@@ -1841,6 +1847,9 @@ func (h *ImportHandler) ImportCoaches(c *gin.Context) {
}
// update existing coach with latest data
if coach.TsID != r.ID {
coach.TsID = r.ID
}
coach.ShortName = strPtrOrNil(r.ShortName)
if r.Birthday != nil {
ts := time.Unix(*r.Birthday, 0).UTC()
......@@ -2195,7 +2204,13 @@ func (h *ImportHandler) ImportPlayers(c *gin.Context) {
for _, r := range payload.Results {
var player models.Player
if err := h.DB.Where("ts_id = ?", r.ID).First(&player).Error; err != nil {
var err error
if r.UID != nil && *r.UID != "" {
err = h.DB.Where("uid = ?", *r.UID).First(&player).Error
} else {
err = h.DB.Where("ts_id = ?", r.ID).First(&player).Error
}
if err != nil {
if err == gorm.ErrRecordNotFound {
// new player
player = models.Player{
......@@ -2271,6 +2286,9 @@ func (h *ImportHandler) ImportPlayers(c *gin.Context) {
}
// update existing
if player.TsID != r.ID {
player.TsID = r.ID
}
player.ShortName = strPtrOrNil(r.ShortName)
if r.Birthday != nil {
ts := time.Unix(*r.Birthday, 0).UTC()
......
......@@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
......@@ -15,11 +16,15 @@ import (
type PlayerHandler struct {
Service services.PlayerService
Teams services.TeamService
Areas services.AreaService
}
func RegisterPlayerRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewPlayerService(db)
h := &PlayerHandler{Service: service}
teamService := services.NewTeamService(db)
areaService := services.NewAreaService(db)
h := &PlayerHandler{Service: service, Teams: teamService, Areas: areaService}
players := rg.Group("/players")
players.GET("", h.List)
......@@ -47,6 +52,7 @@ type Meta struct {
type StructuredPlayer struct {
ID string `json:"id"`
TsID string `json:"tsId"`
WyID *int `json:"wyId"`
GSMID *int `json:"gsmId"`
ShortName string `json:"shortName"`
......@@ -61,6 +67,8 @@ type StructuredPlayer struct {
Foot *string `json:"foot"`
CurrentTeamID *int `json:"currentTeamId"`
CurrentNationalTeamID *int `json:"currentNationalTeamId"`
CountryTsID *string `json:"countryTsId"`
CountryName *string `json:"countryName"`
Gender *string `json:"gender"`
Status string `json:"status"`
JerseyNumber *int `json:"jerseyNumber"`
......@@ -78,6 +86,16 @@ type StructuredPlayer struct {
UID *string `json:"uid"`
Deathday *time.Time `json:"deathday"`
RetireTime *time.Time `json:"retireTime"`
Team *TeamSummary `json:"team"`
}
type TeamSummary struct {
ID string `json:"id"`
TsID string `json:"tsId"`
WyID *int `json:"wyId"`
Name string `json:"name"`
ShortName *string `json:"shortName"`
ImageDataURL *string `json:"imageDataUrl"`
}
func toStructuredPlayer(p models.Player) StructuredPlayer {
......@@ -107,6 +125,7 @@ func toStructuredPlayer(p models.Player) StructuredPlayer {
return StructuredPlayer{
ID: p.ID,
TsID: p.TsID,
WyID: p.WyID,
GSMID: p.GSMID,
ShortName: valueOrDefault(p.ShortName, ""),
......@@ -121,6 +140,8 @@ func toStructuredPlayer(p models.Player) StructuredPlayer {
Foot: p.Foot,
CurrentTeamID: p.CurrentTeamID,
CurrentNationalTeamID: p.CurrentNationalTeamID,
CountryTsID: p.CountryTsID,
CountryName: nil,
Gender: p.Gender,
Status: status,
JerseyNumber: p.JerseyNumber,
......@@ -140,6 +161,51 @@ func toStructuredPlayer(p models.Player) StructuredPlayer {
}
}
func toTeamSummary(t models.Team) TeamSummary {
return TeamSummary{
ID: t.ID,
TsID: t.TsID,
WyID: t.WyID,
Name: t.Name,
ShortName: t.ShortName,
ImageDataURL: t.ImageDataURL,
}
}
func addPlayerTeams(structured []StructuredPlayer, players []models.Player, teamsByTsID map[string]models.Team) {
if len(structured) != len(players) {
return
}
for i := range structured {
if players[i].TeamTsID == nil || strings.TrimSpace(*players[i].TeamTsID) == "" {
continue
}
if t, ok := teamsByTsID[*players[i].TeamTsID]; ok {
ts := toTeamSummary(t)
structured[i].Team = &ts
}
}
}
func addPlayerCountries(structured []StructuredPlayer, players []models.Player, areasByTsID map[string]models.Area) {
if len(structured) != len(players) {
return
}
for i := range structured {
if players[i].CountryTsID == nil {
continue
}
tsID := strings.TrimSpace(*players[i].CountryTsID)
if tsID == "" {
continue
}
if a, ok := areasByTsID[tsID]; ok {
name := a.Name
structured[i].CountryName = &name
}
}
}
func valueOrDefault(s *string, def string) string {
if s != nil {
return *s
......@@ -201,6 +267,64 @@ func (h *PlayerHandler) List(c *gin.Context) {
structured = append(structured, toStructuredPlayer(p))
}
tsIDs := make([]string, 0, len(players))
seen := make(map[string]struct{}, len(players))
for _, p := range players {
if p.TeamTsID == nil {
continue
}
tsID := strings.TrimSpace(*p.TeamTsID)
if tsID == "" {
continue
}
if _, ok := seen[tsID]; ok {
continue
}
seen[tsID] = struct{}{}
tsIDs = append(tsIDs, tsID)
}
if len(tsIDs) > 0 {
teams, err := h.Teams.GetByTsIDs(c.Request.Context(), tsIDs)
if err != nil {
respondError(c, err)
return
}
teamsByTsID := make(map[string]models.Team, len(teams))
for _, t := range teams {
teamsByTsID[t.TsID] = t
}
addPlayerTeams(structured, players, teamsByTsID)
}
countryTsIDs := make([]string, 0, len(players))
countrySeen := make(map[string]struct{}, len(players))
for _, p := range players {
if p.CountryTsID == nil {
continue
}
tsID := strings.TrimSpace(*p.CountryTsID)
if tsID == "" {
continue
}
if _, ok := countrySeen[tsID]; ok {
continue
}
countrySeen[tsID] = struct{}{}
countryTsIDs = append(countryTsIDs, tsID)
}
if len(countryTsIDs) > 0 {
areas, err := h.Areas.GetByTsIDs(c.Request.Context(), countryTsIDs)
if err != nil {
respondError(c, err)
return
}
areasByTsID := make(map[string]models.Area, len(areas))
for _, a := range areas {
areasByTsID[a.TsID] = a
}
addPlayerCountries(structured, players, areasByTsID)
}
page := offset/limit + 1
hasMore := int64(offset+limit) < total
timestamp := time.Now().UTC().Format(time.RFC3339)
......@@ -237,6 +361,34 @@ func (h *PlayerHandler) GetByID(c *gin.Context) {
}
structured := toStructuredPlayer(player)
if player.CountryTsID != nil {
countryTsID := strings.TrimSpace(*player.CountryTsID)
if countryTsID != "" {
areas, err := h.Areas.GetByTsIDs(c.Request.Context(), []string{countryTsID})
if err != nil {
respondError(c, err)
return
}
if len(areas) > 0 {
name := areas[0].Name
structured.CountryName = &name
}
}
}
if player.TeamTsID != nil {
tsID := strings.TrimSpace(*player.TeamTsID)
if tsID != "" {
teams, err := h.Teams.GetByTsIDs(c.Request.Context(), []string{tsID})
if err != nil {
respondError(c, err)
return
}
if len(teams) > 0 {
ts := toTeamSummary(teams[0])
structured.Team = &ts
}
}
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/players/%s", id)
......@@ -261,6 +413,35 @@ func (h *PlayerHandler) GetByAnyProviderID(c *gin.Context) {
}
structured := toStructuredPlayer(player)
if player.CountryTsID != nil {
countryTsID := strings.TrimSpace(*player.CountryTsID)
if countryTsID != "" {
areas, err := h.Areas.GetByTsIDs(c.Request.Context(), []string{countryTsID})
if err != nil {
respondError(c, err)
return
}
if len(areas) > 0 {
name := areas[0].Name
structured.CountryName = &name
}
}
}
if player.TeamTsID != nil {
tsID := strings.TrimSpace(*player.TeamTsID)
if tsID != "" {
teams, err := h.Teams.GetByTsIDs(c.Request.Context(), []string{tsID})
if err != nil {
respondError(c, err)
return
}
if len(teams) > 0 {
ts := toTeamSummary(teams[0])
structured.Team = &ts
}
}
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/players/provider/%s", providerID)
......@@ -298,6 +479,34 @@ func (h *PlayerHandler) GetByProviderID(c *gin.Context) {
}
structured := toStructuredPlayer(player)
if player.CountryTsID != nil {
countryTsID := strings.TrimSpace(*player.CountryTsID)
if countryTsID != "" {
areas, err := h.Areas.GetByTsIDs(c.Request.Context(), []string{countryTsID})
if err != nil {
respondError(c, err)
return
}
if len(areas) > 0 {
name := areas[0].Name
structured.CountryName = &name
}
}
}
if player.TeamTsID != nil {
tsID := strings.TrimSpace(*player.TeamTsID)
if tsID != "" {
teams, err := h.Teams.GetByTsIDs(c.Request.Context(), []string{tsID})
if err != nil {
respondError(c, err)
return
}
if len(teams) > 0 {
ts := toTeamSummary(teams[0])
structured.Team = &ts
}
}
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/players/wyscout/%d", wyID)
......
package handlers
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/services"
)
type SeasonHandler struct {
Service services.SeasonService
}
func RegisterSeasonRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewSeasonService(db)
h := &SeasonHandler{Service: service}
seasons := rg.Group("/seasons")
seasons.GET("", h.List)
seasons.GET("/wyscout/:wyId", h.GetByWyID)
seasons.GET("/provider/:providerId", h.GetByProviderID)
seasons.GET("/:id", h.GetByID)
}
// List seasons
// @Summary List seasons
// @Description Returns a list of seasons with optional pagination.
// @Tags Seasons
// @Param limit query int false "Maximum number of items to return (default 100)"
// @Param offset query int false "Number of items to skip before starting to collect the result set (default 0)"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Router /seasons [get]
func (h *SeasonHandler) List(c *gin.Context) {
limitStr := c.DefaultQuery("limit", "100")
offsetStr := c.DefaultQuery("offset", "0")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
limit = 100
}
offset, err := strconv.Atoi(offsetStr)
if err != nil || offset < 0 {
offset = 0
}
seasons, total, err := h.Service.ListSeasons(c.Request.Context(), limit, offset)
if err != nil {
respondError(c, err)
return
}
page := offset/limit + 1
hasMore := int64(offset+limit) < total
timestamp := time.Now().UTC().Format(time.RFC3339)
c.JSON(http.StatusOK, gin.H{
"data": seasons,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": "/seasons",
"method": "GET",
"totalItems": total,
"page": page,
"limit": limit,
"hasMore": hasMore,
},
})
}
// GetByID returns a single season by internal ID
// @Summary Get season by ID
// @Description Returns a single season by its internal ID.
// @Tags Seasons
// @Param id path string true "Season internal identifier"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /seasons/{id} [get]
func (h *SeasonHandler) GetByID(c *gin.Context) {
id := c.Param("id")
season, err := h.Service.GetByID(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/seasons/%s", id)
c.JSON(http.StatusOK, gin.H{
"data": season,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByProviderID returns a single season by wy_id (numeric) or ts_id (string)
func (h *SeasonHandler) GetByProviderID(c *gin.Context) {
providerID := c.Param("providerId")
season, err := h.Service.GetByAnyProviderID(c.Request.Context(), providerID)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/seasons/provider/%s", providerID)
c.JSON(http.StatusOK, gin.H{
"data": season,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByWyID returns a single season by WyScout WyID
// @Summary Get season by Wyscout ID
// @Description Returns a single season by its Wyscout wy_id identifier.
// @Tags Seasons
// @Param wyId path int true "Wyscout wy_id identifier"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /seasons/wyscout/{wyId} [get]
func (h *SeasonHandler) GetByWyID(c *gin.Context) {
wyIDStr := c.Param("wyId")
wyID, err := strconv.Atoi(wyIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid wyId"})
return
}
season, err := h.Service.GetByWyID(c.Request.Context(), wyID)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/seasons/wyscout/%d", wyID)
c.JSON(http.StatusOK, gin.H{
"data": season,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
......@@ -9,9 +9,20 @@ import (
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/models"
"ScoutingSystemScoreData/internal/services"
)
type TeamListResponse struct {
Data []models.Team `json:"data"`
Meta map[string]interface{} `json:"meta"`
}
type TeamResponse struct {
Data models.Team `json:"data"`
Meta map[string]interface{} `json:"meta"`
}
type TeamHandler struct {
Service services.TeamService
}
......@@ -33,7 +44,7 @@ func RegisterTeamRoutes(rg *gin.RouterGroup, db *gorm.DB) {
// @Tags Teams
// @Param limit query int false "Maximum number of items to return (default 100)"
// @Param offset query int false "Number of items to skip before starting to collect the result set (default 0)"
// @Success 200 {object} map[string]interface{}
// @Success 200 {object} handlers.TeamListResponse
// @Failure 500 {object} map[string]string
// @Router /teams [get]
func (h *TeamHandler) List(c *gin.Context) {
......@@ -49,7 +60,14 @@ func (h *TeamHandler) List(c *gin.Context) {
offset = 0
}
teams, total, err := h.Service.ListTeams(c.Request.Context(), limit, offset)
name := c.Query("name")
endpoint := "/teams"
if name != "" {
endpoint = fmt.Sprintf("/teams?name=%s", name)
}
teams, total, err := h.Service.ListTeams(c.Request.Context(), limit, offset, name)
if err != nil {
respondError(c, err)
return
......@@ -63,7 +81,7 @@ func (h *TeamHandler) List(c *gin.Context) {
"data": teams,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": "/teams",
"endpoint": endpoint,
"method": "GET",
"totalItems": total,
"page": page,
......@@ -78,7 +96,7 @@ func (h *TeamHandler) List(c *gin.Context) {
// @Description Returns a single team by its internal ID.
// @Tags Teams
// @Param id path string true "Team internal identifier"
// @Success 200 {object} map[string]interface{}
// @Success 200 {object} handlers.TeamResponse
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /teams/{id} [get]
......@@ -132,7 +150,7 @@ func (h *TeamHandler) GetByProviderID(c *gin.Context) {
// @Description Returns a single team by its Wyscout wy_id identifier.
// @Tags Teams
// @Param wyId path int true "Wyscout wy_id identifier"
// @Success 200 {object} map[string]interface{}
// @Success 200 {object} handlers.TeamResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
......
package models
import (
"encoding/json"
"time"
gonanoid "github.com/matoous/go-nanoid/v2"
......@@ -109,13 +110,23 @@ type Team struct {
Type string `gorm:"column:type;type:team_type;default:club" json:"type"`
Category string `gorm:"column:category;default:default" json:"category"`
Gender *string `gorm:"column:gender;type:gender" json:"gender"`
CountryTsID *string `gorm:"column:country_ts_id;size:64" json:"countryTsId"`
CompetitionTsID *string `gorm:"column:competition_ts_id;size:64" json:"competitionTsId"`
AreaWyID *int `gorm:"column:area_wy_id" json:"areaWyId"`
City *string `gorm:"column:city" json:"city"`
CoachWyID *int `gorm:"column:coach_wy_id" json:"coachWyId"`
CoachTsID *string `gorm:"column:coach_ts_id;size:64" json:"coachTsId"`
CompetitionWyID *int `gorm:"column:competition_wy_id" json:"competitionWyId"`
SeasonWyID *int `gorm:"column:season_wy_id" json:"seasonWyId"`
League *string `gorm:"column:league" json:"league"`
Season *string `gorm:"column:season" json:"season"`
VenueTsID *string `gorm:"column:venue_ts_id;size:64" json:"venueTsId"`
Website *string `gorm:"column:website" json:"website"`
MarketValue *int `gorm:"column:market_value" json:"marketValue"`
MarketValueCurrency *string `gorm:"column:market_value_currency" json:"marketValueCurrency"`
TotalPlayers *int `gorm:"column:total_players" json:"totalPlayers"`
ForeignPlayers *int `gorm:"column:foreign_players" json:"foreignPlayers"`
NationalPlayers *int `gorm:"column:national_players" json:"nationalPlayers"`
ImageDataURL *string `gorm:"column:image_data_url" json:"imageDataUrl"`
Status string `gorm:"column:status;type:status;default:active" json:"status"`
IsActive bool `gorm:"column:is_active;default:true" json:"isActive"`
......@@ -158,6 +169,8 @@ type Coach struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
WyID *int `gorm:"column:wy_id;uniqueIndex" json:"wyId"`
TsID string `gorm:"column:ts_id;size:64" json:"tsId"`
CountryTsID *string `gorm:"column:country_ts_id;size:64" json:"countryTsId"`
TeamTsID *string `gorm:"column:team_ts_id;size:64" json:"teamTsId"`
FirstName string `gorm:"column:first_name" json:"firstName"`
LastName string `gorm:"column:last_name" json:"lastName"`
MiddleName *string `gorm:"column:middle_name" json:"middleName"`
......@@ -227,6 +240,7 @@ type Player struct {
TsID string `gorm:"column:ts_id;size:64" json:"tsId"`
TeamWyID *int `gorm:"column:team_wy_id" json:"teamWyId"`
TeamTsID *string `gorm:"column:team_ts_id;size:64" json:"teamTsId"`
CountryTsID *string `gorm:"column:country_ts_id;size:64" json:"countryTsId"`
FirstName string `gorm:"column:first_name" json:"firstName"`
LastName string `gorm:"column:last_name" json:"lastName"`
MiddleName *string `gorm:"column:middle_name" json:"middleName"`
......@@ -282,6 +296,8 @@ type Match struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
WyID *int `gorm:"column:wy_id;uniqueIndex" json:"wyId"`
TsID string `gorm:"column:ts_id;size:64;uniqueIndex" json:"tsId"`
SeasonTsID *string `gorm:"column:season_ts_id;size:64" json:"seasonTsId"`
CompetitionTsID *string `gorm:"column:competition_ts_id;size:64" json:"competitionTsId"`
HomeTeamWyID *int `gorm:"column:home_team_wy_id" json:"homeTeamWyId"`
HomeTeamTsID *string `gorm:"column:home_team_ts_id;size:64" json:"homeTeamTsId"`
AwayTeamWyID *int `gorm:"column:away_team_wy_id" json:"awayTeamWyId"`
......@@ -290,6 +306,11 @@ type Match struct {
SeasonWyID *int `gorm:"column:season_wy_id" json:"seasonWyId"`
RoundWyID *int `gorm:"column:round_wy_id" json:"roundWyId"`
MatchDate time.Time `gorm:"column:match_date" json:"matchDate"`
MatchTimeUnix *int64 `gorm:"column:match_time_unix" json:"matchTimeUnix"`
StatusID *int `gorm:"column:status_id" json:"statusId"`
VenueTsID *string `gorm:"column:venue_ts_id;size:64" json:"venueTsId"`
RefereeTsID *string `gorm:"column:referee_ts_id;size:64" json:"refereeTsId"`
Neutral *bool `gorm:"column:neutral" json:"neutral"`
Venue *string `gorm:"column:venue" json:"venue"`
VenueCity *string `gorm:"column:venue_city" json:"venueCity"`
VenueCountry *string `gorm:"column:venue_country" json:"venueCountry"`
......@@ -299,6 +320,17 @@ type Match struct {
AwayScore int `gorm:"column:away_score;default:0" json:"awayScore"`
HomeScorePenalties int `gorm:"column:home_score_penalties;default:0" json:"homeScorePenalties"`
AwayScorePenalties int `gorm:"column:away_score_penalties;default:0" json:"awayScorePenalties"`
HomeScoresJSON json.RawMessage `gorm:"column:home_scores_json;type:jsonb" json:"homeScores" swaggertype:"object"`
AwayScoresJSON json.RawMessage `gorm:"column:away_scores_json;type:jsonb" json:"awayScores" swaggertype:"object"`
HomePosition *string `gorm:"column:home_position" json:"homePosition"`
AwayPosition *string `gorm:"column:away_position" json:"awayPosition"`
CoverageMLive *bool `gorm:"column:coverage_mlive" json:"coverageMlive"`
CoverageLineup *bool `gorm:"column:coverage_lineup" json:"coverageLineup"`
RoundStageTsID *string `gorm:"column:round_stage_ts_id;size:64" json:"roundStageTsId"`
RoundGroupNum *int `gorm:"column:round_group_num" json:"roundGroupNum"`
RoundNum *int `gorm:"column:round_num" json:"roundNum"`
RelatedTsID *string `gorm:"column:related_ts_id;size:64" json:"relatedTsId"`
AggScoreJSON json.RawMessage `gorm:"column:agg_score_json;type:jsonb" json:"aggScore" swaggertype:"object"`
Attendance *int `gorm:"column:attendance" json:"attendance"`
MainRefereeWyID *int `gorm:"column:main_referee_wy_id" json:"mainRefereeWyId"`
AssistantReferee1WyID *int `gorm:"column:assistant_referee_1_wy_id" json:"assistantReferee1WyId"`
......@@ -307,6 +339,15 @@ type Match struct {
VarRefereeWyID *int `gorm:"column:var_referee_wy_id" json:"varRefereeWyId"`
Weather *string `gorm:"column:weather" json:"weather"`
Temperature *float64 `gorm:"column:temperature" json:"temperature"`
Pressure *string `gorm:"column:pressure" json:"pressure"`
Wind *string `gorm:"column:wind" json:"wind"`
Humidity *string `gorm:"column:humidity" json:"humidity"`
TBD *bool `gorm:"column:tbd" json:"tbd"`
HasOT *bool `gorm:"column:has_ot" json:"hasOt"`
EndedUnix *int64 `gorm:"column:ended_unix" json:"endedUnix"`
TeamReverse *bool `gorm:"column:team_reverse" json:"teamReverse"`
Loss *bool `gorm:"column:loss" json:"loss"`
ProviderUpdatedAtUnix *int64 `gorm:"column:provider_updated_at_unix" json:"providerUpdatedAtUnix"`
APILastSyncedAt *time.Time `gorm:"column:api_last_synced_at" json:"apiLastSyncedAt"`
APISyncStatus string `gorm:"column:api_sync_status;type:api_sync_status;default:pending" json:"apiSyncStatus"`
Notes *string `gorm:"column:notes" json:"notes"`
......
......@@ -6,6 +6,7 @@ import (
"os"
"strings"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
......@@ -84,6 +85,21 @@ func basicAuthMiddleware() gin.HandlerFunc {
func New(db *gorm.DB) *gin.Engine {
r := gin.Default()
r.Use(cors.New(cors.Config{
AllowAllOrigins: true,
AllowMethods: []string{
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
http.MethodOptions,
},
AllowHeaders: []string{"*"},
ExposeHeaders: []string{"*"},
AllowCredentials: false,
}))
r.GET("/health", func(c *gin.Context) {
if sqlDB, err := db.DB(); err == nil {
if err := sqlDB.Ping(); err != nil {
......@@ -106,6 +122,8 @@ func New(db *gorm.DB) *gin.Engine {
api.Use(basicAuthMiddleware())
appCfg := config.Load()
handlers.RegisterAreaRoutes(api, db)
handlers.RegisterCompetitionRoutes(api, db)
handlers.RegisterSeasonRoutes(api, db)
handlers.RegisterTeamRoutes(api, db)
handlers.RegisterPlayerRoutes(api, db)
handlers.RegisterCoachRoutes(api, db)
......
......@@ -22,6 +22,7 @@ type AreaService interface {
ListAreas(ctx context.Context, opts ListAreasOptions) ([]models.Area, int64, error)
GetByID(ctx context.Context, id string) (models.Area, error)
GetByProviderID(ctx context.Context, providerID string) (models.Area, error)
GetByTsIDs(ctx context.Context, tsIDs []string) ([]models.Area, error)
}
type areaService struct {
......@@ -88,3 +89,16 @@ func (s *areaService) GetByProviderID(ctx context.Context, providerID string) (m
}
return area, nil
}
func (s *areaService) GetByTsIDs(ctx context.Context, tsIDs []string) ([]models.Area, error) {
if len(tsIDs) == 0 {
return []models.Area{}, nil
}
var areas []models.Area
if err := s.db.WithContext(ctx).Where("ts_id IN ?", tsIDs).Find(&areas).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternal, "failed to fetch areas by ts_id", err)
}
return areas, nil
}
package services
import (
"context"
"strconv"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/errors"
"ScoutingSystemScoreData/internal/models"
)
type CompetitionService interface {
ListCompetitions(ctx context.Context, limit, offset int) ([]models.Competition, int64, error)
GetByID(ctx context.Context, id string) (models.Competition, error)
GetByWyID(ctx context.Context, wyID int) (models.Competition, error)
GetByAnyProviderID(ctx context.Context, providerID string) (models.Competition, error)
}
type competitionService struct {
db *gorm.DB
}
func NewCompetitionService(db *gorm.DB) CompetitionService {
return &competitionService{db: db}
}
func (s *competitionService) ListCompetitions(ctx context.Context, limit, offset int) ([]models.Competition, int64, error) {
var competitions []models.Competition
query := s.db.WithContext(ctx).Model(&models.Competition{})
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to count competitions", err)
}
if err := query.Limit(limit).Offset(offset).Find(&competitions).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to fetch competitions", err)
}
return competitions, total, nil
}
func (s *competitionService) GetByID(ctx context.Context, id string) (models.Competition, error) {
var comp models.Competition
if err := s.db.WithContext(ctx).First(&comp, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Competition{}, errors.New(errors.CodeNotFound, "competition not found")
}
return models.Competition{}, errors.Wrap(errors.CodeInternal, "failed to fetch competition", err)
}
return comp, nil
}
func (s *competitionService) GetByAnyProviderID(ctx context.Context, providerID string) (models.Competition, error) {
// If providerID is numeric, treat as wy_id; otherwise, treat as ts_id
if wyID, err := strconv.Atoi(providerID); err == nil {
return s.GetByWyID(ctx, wyID)
}
var comp models.Competition
if err := s.db.WithContext(ctx).First(&comp, "ts_id = ?", providerID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Competition{}, errors.New(errors.CodeNotFound, "competition not found")
}
return models.Competition{}, errors.Wrap(errors.CodeInternal, "failed to fetch competition", err)
}
return comp, nil
}
func (s *competitionService) GetByWyID(ctx context.Context, wyID int) (models.Competition, error) {
var comp models.Competition
if err := s.db.WithContext(ctx).First(&comp, "wy_id = ?", wyID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Competition{}, errors.New(errors.CodeNotFound, "competition not found")
}
return models.Competition{}, errors.Wrap(errors.CodeInternal, "failed to fetch competition", err)
}
return comp, nil
}
......@@ -2,6 +2,7 @@ package services
import (
"context"
"fmt"
"time"
"gorm.io/gorm"
......@@ -14,7 +15,7 @@ type MatchService interface {
ListMatches(ctx context.Context, opts ListMatchesOptions) ([]models.Match, int64, error)
GetByID(ctx context.Context, id string) (models.Match, error)
GetByTsID(ctx context.Context, matchTsID string) (models.Match, error)
GetHeadToHeadMostRecent(ctx context.Context, teamATsID string, teamBTsID string) (models.Match, error)
GetHeadToHeadMostRecent(ctx context.Context, teamHomeID string, teamAwayID string, from *time.Time, to *time.Time) (models.Match, error)
GetLineup(ctx context.Context, matchTsID string) (models.Match, []models.MatchTeam, []models.MatchLineupPlayer, error)
GetLineupByID(ctx context.Context, id string) (models.Match, []models.MatchTeam, []models.MatchLineupPlayer, error)
}
......@@ -102,15 +103,43 @@ func (s *matchService) GetByID(ctx context.Context, id string) (models.Match, er
return match, nil
}
func (s *matchService) GetHeadToHeadMostRecent(ctx context.Context, teamATsID string, teamBTsID string) (models.Match, error) {
func (s *matchService) GetHeadToHeadMostRecent(ctx context.Context, teamHomeID string, teamAwayID string, from *time.Time, to *time.Time) (models.Match, error) {
var match models.Match
var homeTeam models.Team
if err := s.db.WithContext(ctx).First(&homeTeam, "id = ?", teamHomeID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Match{}, errors.New(errors.CodeNotFound, "home team not found")
}
return models.Match{}, errors.Wrap(errors.CodeInternal, "failed to fetch home team", err)
}
var awayTeam models.Team
if err := s.db.WithContext(ctx).First(&awayTeam, "id = ?", teamAwayID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Match{}, errors.New(errors.CodeNotFound, "away team not found")
}
return models.Match{}, errors.Wrap(errors.CodeInternal, "failed to fetch away team", err)
}
if homeTeam.TsID == "" || awayTeam.TsID == "" {
return models.Match{}, errors.New(errors.CodeNotFound, "team ts_id not found")
}
q := s.db.WithContext(ctx).Model(&models.Match{}).
Where("(home_team_ts_id = ? AND away_team_ts_id = ?) OR (home_team_ts_id = ? AND away_team_ts_id = ?)", teamATsID, teamBTsID, teamBTsID, teamATsID).
Where("home_team_ts_id = ? AND away_team_ts_id = ?", homeTeam.TsID, awayTeam.TsID).
Order("match_date desc")
if from != nil {
q = q.Where("match_date >= ?", *from)
}
if to != nil {
q = q.Where("match_date <= ?", *to)
}
if err := q.First(&match).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Match{}, errors.New(errors.CodeNotFound, "match not found")
return models.Match{}, errors.New(errors.CodeNotFound, fmt.Sprintf("match not found for home=%s away=%s", teamHomeID, teamAwayID))
}
return models.Match{}, errors.Wrap(errors.CodeInternal, "failed to fetch head-to-head match", err)
}
......
......@@ -81,7 +81,13 @@ func (s *playerService) GetByAnyProviderID(ctx context.Context, providerID strin
var player models.Player
if err := s.db.WithContext(ctx).First(&player, "ts_id = ?", providerID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Player{}, errors.New(errors.CodeNotFound, "player not found")
if err := s.db.WithContext(ctx).First(&player, "uid = ?", providerID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Player{}, errors.New(errors.CodeNotFound, "player not found")
}
return models.Player{}, errors.Wrap(errors.CodeInternal, "failed to fetch player", err)
}
return player, nil
}
return models.Player{}, errors.Wrap(errors.CodeInternal, "failed to fetch player", err)
}
......
package services
import (
"context"
"strconv"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/errors"
"ScoutingSystemScoreData/internal/models"
)
type SeasonService interface {
ListSeasons(ctx context.Context, limit, offset int) ([]models.Season, int64, error)
GetByID(ctx context.Context, id string) (models.Season, error)
GetByWyID(ctx context.Context, wyID int) (models.Season, error)
GetByAnyProviderID(ctx context.Context, providerID string) (models.Season, error)
}
type seasonService struct {
db *gorm.DB
}
func NewSeasonService(db *gorm.DB) SeasonService {
return &seasonService{db: db}
}
func (s *seasonService) ListSeasons(ctx context.Context, limit, offset int) ([]models.Season, int64, error) {
var seasons []models.Season
query := s.db.WithContext(ctx).Model(&models.Season{})
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to count seasons", err)
}
if err := query.Limit(limit).Offset(offset).Find(&seasons).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to fetch seasons", err)
}
return seasons, total, nil
}
func (s *seasonService) GetByID(ctx context.Context, id string) (models.Season, error) {
var season models.Season
if err := s.db.WithContext(ctx).First(&season, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Season{}, errors.New(errors.CodeNotFound, "season not found")
}
return models.Season{}, errors.Wrap(errors.CodeInternal, "failed to fetch season", err)
}
return season, nil
}
func (s *seasonService) GetByAnyProviderID(ctx context.Context, providerID string) (models.Season, error) {
// If providerID is numeric, treat as wy_id; otherwise, treat as ts_id
if wyID, err := strconv.Atoi(providerID); err == nil {
return s.GetByWyID(ctx, wyID)
}
var season models.Season
if err := s.db.WithContext(ctx).First(&season, "ts_id = ?", providerID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Season{}, errors.New(errors.CodeNotFound, "season not found")
}
return models.Season{}, errors.Wrap(errors.CodeInternal, "failed to fetch season", err)
}
return season, nil
}
func (s *seasonService) GetByWyID(ctx context.Context, wyID int) (models.Season, error) {
var season models.Season
if err := s.db.WithContext(ctx).First(&season, "wy_id = ?", wyID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Season{}, errors.New(errors.CodeNotFound, "season not found")
}
return models.Season{}, errors.Wrap(errors.CodeInternal, "failed to fetch season", err)
}
return season, nil
}
......@@ -11,10 +11,11 @@ import (
)
type TeamService interface {
ListTeams(ctx context.Context, limit, offset int) ([]models.Team, int64, error)
ListTeams(ctx context.Context, limit, offset int, name string) ([]models.Team, int64, error)
GetByID(ctx context.Context, id string) (models.Team, error)
GetByWyID(ctx context.Context, wyID int) (models.Team, error)
GetByAnyProviderID(ctx context.Context, providerID string) (models.Team, error)
GetByTsIDs(ctx context.Context, tsIDs []string) ([]models.Team, error)
}
type teamService struct {
......@@ -25,9 +26,12 @@ func NewTeamService(db *gorm.DB) TeamService {
return &teamService{db: db}
}
func (s *teamService) ListTeams(ctx context.Context, limit, offset int) ([]models.Team, int64, error) {
func (s *teamService) ListTeams(ctx context.Context, limit, offset int, name string) ([]models.Team, int64, error) {
var teams []models.Team
query := s.db.WithContext(ctx).Model(&models.Team{})
if name != "" {
query = query.Where("name ILIKE ?", "%"+name+"%")
}
var total int64
if err := query.Count(&total).Error; err != nil {
......@@ -78,3 +82,16 @@ func (s *teamService) GetByWyID(ctx context.Context, wyID int) (models.Team, err
}
return team, nil
}
func (s *teamService) GetByTsIDs(ctx context.Context, tsIDs []string) ([]models.Team, error) {
if len(tsIDs) == 0 {
return []models.Team{}, nil
}
var teams []models.Team
if err := s.db.WithContext(ctx).Where("ts_id IN ?", tsIDs).Find(&teams).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternal, "failed to fetch teams by ts_id", err)
}
return teams, nil
}
-- Migration 0005: add TheSports country_id field to players
ALTER TABLE players
ADD COLUMN IF NOT EXISTS country_ts_id varchar(64);
ALTER TABLE teams
ADD COLUMN IF NOT EXISTS country_ts_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS competition_ts_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS coach_ts_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS venue_ts_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS website TEXT,
ADD COLUMN IF NOT EXISTS market_value INTEGER,
ADD COLUMN IF NOT EXISTS market_value_currency TEXT,
ADD COLUMN IF NOT EXISTS total_players INTEGER,
ADD COLUMN IF NOT EXISTS foreign_players INTEGER,
ADD COLUMN IF NOT EXISTS national_players INTEGER;
ALTER TABLE coaches
ADD COLUMN IF NOT EXISTS country_ts_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS team_ts_id VARCHAR(64);
File added
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment