Commit c2aacc7f by Augusto

new migrations

parent 90a1bdcf
...@@ -28,14 +28,6 @@ definitions: ...@@ -28,14 +28,6 @@ definitions:
additionalProperties: true additionalProperties: true
type: object type: object
type: object type: object
handlers.MatchResponse:
properties:
data:
$ref: '#/definitions/handlers.matchOut'
meta:
additionalProperties: true
type: object
type: object
handlers.StructuredCoach: handlers.StructuredCoach:
properties: properties:
apiLastSyncedAt: apiLastSyncedAt:
...@@ -117,6 +109,95 @@ definitions: ...@@ -117,6 +109,95 @@ definitions:
additionalProperties: true additionalProperties: true
type: object type: object
type: object type: object
handlers.competitionGetResponse:
properties:
data:
$ref: '#/definitions/handlers.competitionResponse'
meta:
additionalProperties: true
type: object
type: object
handlers.competitionResponse:
properties:
areaWyId:
type: integer
category:
type: string
countryName:
type: string
countryTsId:
type: string
createdAt:
type: string
curRound:
type: integer
curSeasonTsId:
type: string
curStageTsId:
type: string
deletedAt:
type: string
divisions:
items:
type: object
type: array
gender:
type: string
host:
type: object
id:
type: string
isActive:
type: boolean
logo:
type: string
mostTitles:
items:
type: object
type: array
name:
type: string
newcomers:
items:
type: object
type: array
officialName:
type: string
roundCount:
type: integer
shortName:
type: string
theSportsUpdatedAt:
type: string
titleHolder:
items:
type: object
type: array
tsCategoryId:
type: string
tsId:
type: string
tsType:
type: integer
type:
type: string
uid:
type: string
updatedAt:
type: string
wyId:
type: integer
type: object
handlers.competitionsListResponse:
properties:
data:
items:
$ref: '#/definitions/handlers.competitionResponse'
type: array
meta:
additionalProperties: true
type: object
type: object
handlers.matchOut: handlers.matchOut:
properties: properties:
aggScore: aggScore:
...@@ -587,12 +668,19 @@ paths: ...@@ -587,12 +668,19 @@ paths:
in: query in: query
name: offset name: offset
type: integer type: integer
- description: Filter by competition name (case-insensitive, partial match)
in: query
name: name
type: string
- description: Filter by TheSports country id (matches competitions.country_ts_id)
in: query
name: countryId
type: string
responses: responses:
"200": "200":
description: OK description: OK
schema: schema:
additionalProperties: true $ref: '#/definitions/handlers.competitionsListResponse'
type: object
"500": "500":
description: Internal Server Error description: Internal Server Error
schema: schema:
...@@ -615,8 +703,7 @@ paths: ...@@ -615,8 +703,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
additionalProperties: true $ref: '#/definitions/handlers.competitionGetResponse'
type: object
"404": "404":
description: Not Found description: Not Found
schema: schema:
...@@ -632,6 +719,36 @@ paths: ...@@ -632,6 +719,36 @@ paths:
summary: Get competition by ID summary: Get competition by ID
tags: tags:
- Competitions - Competitions
/competitions/provider/{providerId}:
get:
description: Returns a single competition by provider ID. If providerId is numeric
it is treated as Wyscout wy_id, otherwise it is treated as TheSports ts_id.
parameters:
- description: Provider identifier (wy_id or ts_id)
in: path
name: providerId
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.competitionGetResponse'
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get competition by provider ID
tags:
- Competitions
/competitions/wyscout/{wyId}: /competitions/wyscout/{wyId}:
get: get:
description: Returns a single competition by its Wyscout wy_id identifier. description: Returns a single competition by its Wyscout wy_id identifier.
...@@ -645,8 +762,7 @@ paths: ...@@ -645,8 +762,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
additionalProperties: true $ref: '#/definitions/handlers.competitionGetResponse'
type: object
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
...@@ -1002,7 +1118,7 @@ paths: ...@@ -1002,7 +1118,7 @@ paths:
/matches: /matches:
get: get:
description: Returns a paginated list of matches, optionally filtered by competitionWyId, description: Returns a paginated list of matches, optionally filtered by competitionWyId,
seasonWyId, teamTsId and date range. seasonWyId, teamTsId/teamId (+teamSide) and date range.
parameters: parameters:
- description: Maximum number of items to return (default 100) - description: Maximum number of items to return (default 100)
in: query in: query
...@@ -1025,6 +1141,15 @@ paths: ...@@ -1025,6 +1141,15 @@ paths:
in: query in: query
name: teamTsId name: teamTsId
type: string type: string
- description: Filter by internal team id (matches where team is home or away)
in: query
name: teamId
type: string
- description: 'When filtering by teamTsId/teamId, restrict side: home|away|either
(default either)'
in: query
name: teamSide
type: string
- description: Filter matches on/after date (RFC3339 or YYYY-MM-DD) - description: Filter matches on/after date (RFC3339 or YYYY-MM-DD)
in: query in: query
name: from name: from
...@@ -1094,8 +1219,8 @@ paths: ...@@ -1094,8 +1219,8 @@ paths:
- Matches - Matches
/matches/head-to-head: /matches/head-to-head:
get: get:
description: Returns the most recent match where teamHome played at home and description: Returns a paginated list of matches where teamHome played at home
teamAway played away (by internal team id). and teamAway played away (by internal team id).
parameters: parameters:
- description: Home team internal id - description: Home team internal id
in: query in: query
...@@ -1107,6 +1232,15 @@ paths: ...@@ -1107,6 +1232,15 @@ paths:
name: teamAway name: teamAway
required: true required: true
type: string type: string
- description: Maximum number of items to return (default 100)
in: query
name: limit
type: integer
- description: Number of items to skip before starting to collect the result
set (default 0)
in: query
name: offset
type: integer
- description: Filter matches on/after date (RFC3339 or YYYY-MM-DD) - description: Filter matches on/after date (RFC3339 or YYYY-MM-DD)
in: query in: query
name: from name: from
...@@ -1119,7 +1253,7 @@ paths: ...@@ -1119,7 +1253,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/handlers.MatchResponse' $ref: '#/definitions/handlers.MatchListResponse'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
...@@ -1138,7 +1272,7 @@ paths: ...@@ -1138,7 +1272,7 @@ paths:
additionalProperties: additionalProperties:
type: string type: string
type: object type: object
summary: Most recent match between two teams summary: List matches between two teams
tags: tags:
- Matches - Matches
/matches/id/{id}/lineup: /matches/id/{id}/lineup:
......
...@@ -9,16 +9,52 @@ import ( ...@@ -9,16 +9,52 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"ScoutingSystemScoreData/internal/models"
"ScoutingSystemScoreData/internal/services" "ScoutingSystemScoreData/internal/services"
) )
type competitionResponse struct {
models.Competition
CountryName *string `json:"countryName"`
}
type competitionsListResponse struct {
Data []competitionResponse `json:"data"`
Meta map[string]interface{} `json:"meta"`
}
type competitionGetResponse struct {
Data competitionResponse `json:"data"`
Meta map[string]interface{} `json:"meta"`
}
func lookupCountryNamesByTsID(db *gorm.DB, countryTsIDs []string) (map[string]string, error) {
if len(countryTsIDs) == 0 {
return map[string]string{}, nil
}
var areas []models.Area
if err := db.Select("ts_id", "name").Where("ts_id IN ?", countryTsIDs).Find(&areas).Error; err != nil {
return nil, err
}
res := make(map[string]string, len(areas))
for _, a := range areas {
if a.TsID != "" {
res[a.TsID] = a.Name
}
}
return res, nil
}
type CompetitionHandler struct { type CompetitionHandler struct {
Service services.CompetitionService Service services.CompetitionService
DB *gorm.DB
} }
func RegisterCompetitionRoutes(rg *gin.RouterGroup, db *gorm.DB) { func RegisterCompetitionRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewCompetitionService(db) service := services.NewCompetitionService(db)
h := &CompetitionHandler{Service: service} h := &CompetitionHandler{Service: service, DB: db}
competitions := rg.Group("/competitions") competitions := rg.Group("/competitions")
competitions.GET("", h.List) competitions.GET("", h.List)
...@@ -33,12 +69,16 @@ func RegisterCompetitionRoutes(rg *gin.RouterGroup, db *gorm.DB) { ...@@ -33,12 +69,16 @@ func RegisterCompetitionRoutes(rg *gin.RouterGroup, db *gorm.DB) {
// @Tags Competitions // @Tags Competitions
// @Param limit query int false "Maximum number of items to return (default 100)" // @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)" // @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{} // @Param name query string false "Filter by competition name (case-insensitive, partial match)"
// @Param countryId query string false "Filter by TheSports country id (matches competitions.country_ts_id)"
// @Success 200 {object} competitionsListResponse
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Router /competitions [get] // @Router /competitions [get]
func (h *CompetitionHandler) List(c *gin.Context) { func (h *CompetitionHandler) List(c *gin.Context) {
limitStr := c.DefaultQuery("limit", "100") limitStr := c.DefaultQuery("limit", "100")
offsetStr := c.DefaultQuery("offset", "0") offsetStr := c.DefaultQuery("offset", "0")
nameStr := c.Query("name")
countryIDStr := c.Query("countryId")
limit, err := strconv.Atoi(limitStr) limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 { if err != nil || limit <= 0 {
...@@ -49,18 +89,52 @@ func (h *CompetitionHandler) List(c *gin.Context) { ...@@ -49,18 +89,52 @@ func (h *CompetitionHandler) List(c *gin.Context) {
offset = 0 offset = 0
} }
competitions, total, err := h.Service.ListCompetitions(c.Request.Context(), limit, offset) var name *string
if nameStr != "" {
name = &nameStr
}
var countryTsID *string
if countryIDStr != "" {
countryTsID = &countryIDStr
}
competitions, total, err := h.Service.ListCompetitions(c.Request.Context(), limit, offset, name, countryTsID)
if err != nil { if err != nil {
respondError(c, err) respondError(c, err)
return return
} }
countryIDs := make([]string, 0, len(competitions))
seen := make(map[string]struct{}, len(competitions))
for _, comp := range competitions {
if comp.CountryTsID != nil && *comp.CountryTsID != "" {
if _, ok := seen[*comp.CountryTsID]; !ok {
seen[*comp.CountryTsID] = struct{}{}
countryIDs = append(countryIDs, *comp.CountryTsID)
}
}
}
countryNames, err := lookupCountryNamesByTsID(h.DB, countryIDs)
_ = err
data := make([]competitionResponse, 0, len(competitions))
for _, comp := range competitions {
var countryName *string
if comp.CountryTsID != nil {
if n, ok := countryNames[*comp.CountryTsID]; ok {
v := n
countryName = &v
}
}
data = append(data, competitionResponse{Competition: comp, CountryName: countryName})
}
page := offset/limit + 1 page := offset/limit + 1
hasMore := int64(offset+limit) < total hasMore := int64(offset+limit) < total
timestamp := time.Now().UTC().Format(time.RFC3339) timestamp := time.Now().UTC().Format(time.RFC3339)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"data": competitions, "data": data,
"meta": gin.H{ "meta": gin.H{
"timestamp": timestamp, "timestamp": timestamp,
"endpoint": "/competitions", "endpoint": "/competitions",
...@@ -78,7 +152,7 @@ func (h *CompetitionHandler) List(c *gin.Context) { ...@@ -78,7 +152,7 @@ func (h *CompetitionHandler) List(c *gin.Context) {
// @Description Returns a single competition by its internal ID. // @Description Returns a single competition by its internal ID.
// @Tags Competitions // @Tags Competitions
// @Param id path string true "Competition internal identifier" // @Param id path string true "Competition internal identifier"
// @Success 200 {object} map[string]interface{} // @Success 200 {object} competitionGetResponse
// @Failure 404 {object} map[string]string // @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Router /competitions/{id} [get] // @Router /competitions/{id} [get]
...@@ -91,11 +165,20 @@ func (h *CompetitionHandler) GetByID(c *gin.Context) { ...@@ -91,11 +165,20 @@ func (h *CompetitionHandler) GetByID(c *gin.Context) {
return return
} }
var countryName *string
if comp.CountryTsID != nil && *comp.CountryTsID != "" {
var area models.Area
if err := h.DB.Select("name").Where("ts_id = ?", *comp.CountryTsID).First(&area).Error; err == nil {
v := area.Name
countryName = &v
}
}
timestamp := time.Now().UTC().Format(time.RFC3339) timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/competitions/%s", id) endpoint := fmt.Sprintf("/competitions/%s", id)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"data": comp, "data": competitionResponse{Competition: comp, CountryName: countryName},
"meta": gin.H{ "meta": gin.H{
"timestamp": timestamp, "timestamp": timestamp,
"endpoint": endpoint, "endpoint": endpoint,
...@@ -105,6 +188,14 @@ func (h *CompetitionHandler) GetByID(c *gin.Context) { ...@@ -105,6 +188,14 @@ func (h *CompetitionHandler) GetByID(c *gin.Context) {
} }
// GetByProviderID returns a single competition by wy_id (numeric) or ts_id (string) // GetByProviderID returns a single competition by wy_id (numeric) or ts_id (string)
// @Summary Get competition by provider ID
// @Description Returns a single competition by provider ID. If providerId is numeric it is treated as Wyscout wy_id, otherwise it is treated as TheSports ts_id.
// @Tags Competitions
// @Param providerId path string true "Provider identifier (wy_id or ts_id)"
// @Success 200 {object} competitionGetResponse
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /competitions/provider/{providerId} [get]
func (h *CompetitionHandler) GetByProviderID(c *gin.Context) { func (h *CompetitionHandler) GetByProviderID(c *gin.Context) {
providerID := c.Param("providerId") providerID := c.Param("providerId")
...@@ -114,11 +205,20 @@ func (h *CompetitionHandler) GetByProviderID(c *gin.Context) { ...@@ -114,11 +205,20 @@ func (h *CompetitionHandler) GetByProviderID(c *gin.Context) {
return return
} }
var countryName *string
if comp.CountryTsID != nil && *comp.CountryTsID != "" {
var area models.Area
if err := h.DB.Select("name").Where("ts_id = ?", *comp.CountryTsID).First(&area).Error; err == nil {
v := area.Name
countryName = &v
}
}
timestamp := time.Now().UTC().Format(time.RFC3339) timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/competitions/provider/%s", providerID) endpoint := fmt.Sprintf("/competitions/provider/%s", providerID)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"data": comp, "data": competitionResponse{Competition: comp, CountryName: countryName},
"meta": gin.H{ "meta": gin.H{
"timestamp": timestamp, "timestamp": timestamp,
"endpoint": endpoint, "endpoint": endpoint,
...@@ -132,7 +232,7 @@ func (h *CompetitionHandler) GetByProviderID(c *gin.Context) { ...@@ -132,7 +232,7 @@ func (h *CompetitionHandler) GetByProviderID(c *gin.Context) {
// @Description Returns a single competition by its Wyscout wy_id identifier. // @Description Returns a single competition by its Wyscout wy_id identifier.
// @Tags Competitions // @Tags Competitions
// @Param wyId path int true "Wyscout wy_id identifier" // @Param wyId path int true "Wyscout wy_id identifier"
// @Success 200 {object} map[string]interface{} // @Success 200 {object} competitionGetResponse
// @Failure 400 {object} map[string]string // @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string // @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
...@@ -151,11 +251,20 @@ func (h *CompetitionHandler) GetByWyID(c *gin.Context) { ...@@ -151,11 +251,20 @@ func (h *CompetitionHandler) GetByWyID(c *gin.Context) {
return return
} }
var countryName *string
if comp.CountryTsID != nil && *comp.CountryTsID != "" {
var area models.Area
if err := h.DB.Select("name").Where("ts_id = ?", *comp.CountryTsID).First(&area).Error; err == nil {
v := area.Name
countryName = &v
}
}
timestamp := time.Now().UTC().Format(time.RFC3339) timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/competitions/wyscout/%d", wyID) endpoint := fmt.Sprintf("/competitions/wyscout/%d", wyID)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"data": comp, "data": competitionResponse{Competition: comp, CountryName: countryName},
"meta": gin.H{ "meta": gin.H{
"timestamp": timestamp, "timestamp": timestamp,
"endpoint": endpoint, "endpoint": endpoint,
......
...@@ -1306,6 +1306,14 @@ func (h *ImportHandler) ImportCompetitions(c *gin.Context) { ...@@ -1306,6 +1306,14 @@ func (h *ImportHandler) ImportCompetitions(c *gin.Context) {
Logo *string `json:"logo"` Logo *string `json:"logo"`
Type int `json:"type"` Type int `json:"type"`
CurSeasonID string `json:"cur_season_id"` CurSeasonID string `json:"cur_season_id"`
CurStageID string `json:"cur_stage_id"`
CurRound *int `json:"cur_round"`
RoundCount *int `json:"round_count"`
TitleHolder json.RawMessage `json:"title_holder"`
MostTitles json.RawMessage `json:"most_titles"`
Newcomers json.RawMessage `json:"newcomers"`
Divisions json.RawMessage `json:"divisions"`
Host json.RawMessage `json:"host"`
Gender *int `json:"gender"` Gender *int `json:"gender"`
UID *string `json:"uid"` UID *string `json:"uid"`
UpdatedAt *int64 `json:"updated_at"` UpdatedAt *int64 `json:"updated_at"`
...@@ -1330,6 +1338,43 @@ func (h *ImportHandler) ImportCompetitions(c *gin.Context) { ...@@ -1330,6 +1338,43 @@ func (h *ImportHandler) ImportCompetitions(c *gin.Context) {
TsID: r.ID, TsID: r.ID,
Name: r.Name, Name: r.Name,
} }
if r.CategoryID != "" {
v := r.CategoryID
comp.TsCategoryID = &v
}
if r.CountryID != "" {
v := r.CountryID
comp.CountryTsID = &v
}
if r.ShortName != "" {
v := r.ShortName
comp.ShortName = &v
}
comp.Logo = r.Logo
if r.Type != 0 {
v := r.Type
comp.TsType = &v
}
if r.CurSeasonID != "" {
v := r.CurSeasonID
comp.CurSeasonTsID = &v
}
if r.CurStageID != "" {
v := r.CurStageID
comp.CurStageTsID = &v
}
comp.CurRound = r.CurRound
comp.RoundCount = r.RoundCount
comp.TitleHolderJSON = r.TitleHolder
comp.MostTitlesJSON = r.MostTitles
comp.NewcomersJSON = r.Newcomers
comp.DivisionsJSON = r.Divisions
comp.HostJSON = r.Host
comp.UID = r.UID
if r.UpdatedAt != nil {
ts := time.Unix(*r.UpdatedAt, 0).UTC()
comp.TheSportsUpdatedAt = &ts
}
// gender mapping // gender mapping
if r.Gender != nil { if r.Gender != nil {
gender := "" gender := ""
...@@ -1365,6 +1410,57 @@ func (h *ImportHandler) ImportCompetitions(c *gin.Context) { ...@@ -1365,6 +1410,57 @@ func (h *ImportHandler) ImportCompetitions(c *gin.Context) {
// update existing competition // update existing competition
comp.Name = r.Name comp.Name = r.Name
if r.CategoryID != "" {
v := r.CategoryID
comp.TsCategoryID = &v
} else {
comp.TsCategoryID = nil
}
if r.CountryID != "" {
v := r.CountryID
comp.CountryTsID = &v
} else {
comp.CountryTsID = nil
}
if r.ShortName != "" {
v := r.ShortName
comp.ShortName = &v
} else {
comp.ShortName = nil
}
comp.Logo = r.Logo
if r.Type != 0 {
v := r.Type
comp.TsType = &v
} else {
comp.TsType = nil
}
if r.CurSeasonID != "" {
v := r.CurSeasonID
comp.CurSeasonTsID = &v
} else {
comp.CurSeasonTsID = nil
}
if r.CurStageID != "" {
v := r.CurStageID
comp.CurStageTsID = &v
} else {
comp.CurStageTsID = nil
}
comp.CurRound = r.CurRound
comp.RoundCount = r.RoundCount
comp.TitleHolderJSON = r.TitleHolder
comp.MostTitlesJSON = r.MostTitles
comp.NewcomersJSON = r.Newcomers
comp.DivisionsJSON = r.Divisions
comp.HostJSON = r.Host
comp.UID = r.UID
if r.UpdatedAt != nil {
ts := time.Unix(*r.UpdatedAt, 0).UTC()
comp.TheSportsUpdatedAt = &ts
} else {
comp.TheSportsUpdatedAt = nil
}
if r.Gender != nil { if r.Gender != nil {
gender := "" gender := ""
switch *r.Gender { switch *r.Gender {
......
...@@ -333,13 +333,15 @@ func (h *MatchHandler) GetLineupByMatchID(c *gin.Context) { ...@@ -333,13 +333,15 @@ func (h *MatchHandler) GetLineupByMatchID(c *gin.Context) {
// List matches // List matches
// @Summary List matches // @Summary List matches
// @Description Returns a paginated list of matches, optionally filtered by competitionWyId, seasonWyId, teamTsId and date range. // @Description Returns a paginated list of matches, optionally filtered by competitionWyId, seasonWyId, teamTsId/teamId (+teamSide) and date range.
// @Tags Matches // @Tags Matches
// @Param limit query int false "Maximum number of items to return (default 100)" // @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)" // @Param offset query int false "Number of items to skip before starting to collect the result set (default 0)"
// @Param competitionWyId query int false "Filter by competition wy_id" // @Param competitionWyId query int false "Filter by competition wy_id"
// @Param seasonWyId query int false "Filter by season wy_id" // @Param seasonWyId query int false "Filter by season wy_id"
// @Param teamTsId query string false "Filter by team ts_id (matches where team is home or away)" // @Param teamTsId query string false "Filter by team ts_id (matches where team is home or away)"
// @Param teamId query string false "Filter by internal team id (matches where team is home or away)"
// @Param teamSide query string false "When filtering by teamTsId/teamId, restrict side: home|away|either (default either)"
// @Param from query string false "Filter matches on/after date (RFC3339 or YYYY-MM-DD)" // @Param from query string false "Filter matches on/after date (RFC3339 or YYYY-MM-DD)"
// @Param to query string false "Filter matches on/before date (RFC3339 or YYYY-MM-DD)" // @Param to query string false "Filter matches on/before date (RFC3339 or YYYY-MM-DD)"
// @Param status query string false "Filter by match status" // @Param status query string false "Filter by match status"
...@@ -386,6 +388,21 @@ func (h *MatchHandler) List(c *gin.Context) { ...@@ -386,6 +388,21 @@ func (h *MatchHandler) List(c *gin.Context) {
teamTsID = &v teamTsID = &v
} }
var teamID *string
if v := c.Query("teamId"); v != "" {
teamID = &v
}
if teamTsID != nil && teamID != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "use either teamTsId or teamId, not both"})
return
}
var teamSide *string
if v := c.Query("teamSide"); v != "" {
teamSide = &v
}
parseDate := func(raw string) (*time.Time, error) { parseDate := func(raw string) (*time.Time, error) {
if raw == "" { if raw == "" {
return nil, nil return nil, nil
...@@ -429,6 +446,8 @@ func (h *MatchHandler) List(c *gin.Context) { ...@@ -429,6 +446,8 @@ func (h *MatchHandler) List(c *gin.Context) {
CompetitionWyID: competitionWyID, CompetitionWyID: competitionWyID,
SeasonWyID: seasonWyID, SeasonWyID: seasonWyID,
TeamTsID: teamTsID, TeamTsID: teamTsID,
TeamID: teamID,
TeamSide: teamSide,
FromDate: from, FromDate: from,
ToDate: to, ToDate: to,
Status: status, Status: status,
...@@ -468,14 +487,16 @@ func (h *MatchHandler) List(c *gin.Context) { ...@@ -468,14 +487,16 @@ func (h *MatchHandler) List(c *gin.Context) {
} }
// HeadToHeadMostRecent // HeadToHeadMostRecent
// @Summary Most recent match between two teams // @Summary List matches between two teams
// @Description Returns the most recent match where teamHome played at home and teamAway played away (by internal team id). // @Description Returns a paginated list of matches where teamHome played at home and teamAway played away (by internal team id).
// @Tags Matches // @Tags Matches
// @Param teamHome query string true "Home team internal id" // @Param teamHome query string true "Home team internal id"
// @Param teamAway query string true "Away team internal id" // @Param teamAway query string true "Away team internal id"
// @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)"
// @Param from query string false "Filter matches on/after date (RFC3339 or YYYY-MM-DD)" // @Param from query string false "Filter matches on/after date (RFC3339 or YYYY-MM-DD)"
// @Param to query string false "Filter matches on/before date (RFC3339 or YYYY-MM-DD)" // @Param to query string false "Filter matches on/before date (RFC3339 or YYYY-MM-DD)"
// @Success 200 {object} handlers.MatchResponse // @Success 200 {object} handlers.MatchListResponse
// @Failure 400 {object} map[string]string // @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string // @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
...@@ -483,6 +504,17 @@ func (h *MatchHandler) List(c *gin.Context) { ...@@ -483,6 +504,17 @@ func (h *MatchHandler) List(c *gin.Context) {
func (h *MatchHandler) HeadToHeadMostRecent(c *gin.Context) { func (h *MatchHandler) HeadToHeadMostRecent(c *gin.Context) {
teamHome := c.Query("teamHome") teamHome := c.Query("teamHome")
teamAway := c.Query("teamAway") teamAway := c.Query("teamAway")
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
}
parseDate := func(raw string) (*time.Time, error) { parseDate := func(raw string) (*time.Time, error) {
if raw == "" { if raw == "" {
...@@ -524,20 +556,22 @@ func (h *MatchHandler) HeadToHeadMostRecent(c *gin.Context) { ...@@ -524,20 +556,22 @@ func (h *MatchHandler) HeadToHeadMostRecent(c *gin.Context) {
return return
} }
match, err := h.Service.GetHeadToHeadMostRecent(c.Request.Context(), teamHome, teamAway, from, to) matches, total, err := h.Service.ListHeadToHead(c.Request.Context(), teamHome, teamAway, from, to, limit, offset)
if err != nil { if err != nil {
respondError(c, err) respondError(c, err)
return return
} }
matchWithNames, err := h.enrichMatch(c.Request.Context(), match) matchesWithNames, err := h.enrichMatches(c.Request.Context(), matches)
if err != nil { if err != nil {
respondError(c, err) respondError(c, err)
return return
} }
page := offset/limit + 1
hasMore := int64(offset+limit) < total
timestamp := time.Now().UTC().Format(time.RFC3339) timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/matches/head-to-head?teamHome=%s&teamAway=%s", teamHome, teamAway) endpoint := fmt.Sprintf("/matches/head-to-head?teamHome=%s&teamAway=%s&limit=%d&offset=%d", teamHome, teamAway, limit, offset)
if from != nil { if from != nil {
endpoint = fmt.Sprintf("%s&from=%s", endpoint, from.UTC().Format(time.RFC3339)) endpoint = fmt.Sprintf("%s&from=%s", endpoint, from.UTC().Format(time.RFC3339))
} }
...@@ -546,11 +580,15 @@ func (h *MatchHandler) HeadToHeadMostRecent(c *gin.Context) { ...@@ -546,11 +580,15 @@ func (h *MatchHandler) HeadToHeadMostRecent(c *gin.Context) {
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"data": matchWithNames, "data": matchesWithNames,
"meta": gin.H{ "meta": gin.H{
"timestamp": timestamp, "timestamp": timestamp,
"endpoint": endpoint, "endpoint": endpoint,
"method": "GET", "method": "GET",
"totalItems": total,
"page": page,
"limit": limit,
"hasMore": hasMore,
}, },
}) })
} }
......
...@@ -43,10 +43,26 @@ type Competition struct { ...@@ -43,10 +43,26 @@ type Competition struct {
ID string `gorm:"primaryKey;size:16" json:"id"` ID string `gorm:"primaryKey;size:16" json:"id"`
WyID *int `gorm:"column:wy_id" json:"wyId"` WyID *int `gorm:"column:wy_id" json:"wyId"`
TsID string `gorm:"column:ts_id;size:64;uniqueIndex" json:"tsId"` TsID string `gorm:"column:ts_id;size:64;uniqueIndex" json:"tsId"`
TsCategoryID *string `gorm:"column:ts_category_id;size:64" json:"tsCategoryId"`
CountryTsID *string `gorm:"column:country_ts_id;size:64" json:"countryTsId"`
Name string `gorm:"column:name" json:"name"` Name string `gorm:"column:name" json:"name"`
ShortName *string `gorm:"column:short_name" json:"shortName"`
Logo *string `gorm:"column:logo" json:"logo"`
OfficialName *string `gorm:"column:official_name" json:"officialName"` OfficialName *string `gorm:"column:official_name" json:"officialName"`
AreaWyID *int `gorm:"column:area_wy_id" json:"areaWyId"` AreaWyID *int `gorm:"column:area_wy_id" json:"areaWyId"`
Gender *string `gorm:"column:gender;type:gender" json:"gender"` Gender *string `gorm:"column:gender;type:gender" json:"gender"`
TsType *int `gorm:"column:ts_type" json:"tsType"`
CurSeasonTsID *string `gorm:"column:cur_season_ts_id;size:64" json:"curSeasonTsId"`
CurStageTsID *string `gorm:"column:cur_stage_ts_id;size:64" json:"curStageTsId"`
CurRound *int `gorm:"column:cur_round" json:"curRound"`
RoundCount *int `gorm:"column:round_count" json:"roundCount"`
TitleHolderJSON json.RawMessage `gorm:"column:title_holder_json;type:jsonb" json:"titleHolder" swaggertype:"array,object"`
MostTitlesJSON json.RawMessage `gorm:"column:most_titles_json;type:jsonb" json:"mostTitles" swaggertype:"array,object"`
NewcomersJSON json.RawMessage `gorm:"column:newcomers_json;type:jsonb" json:"newcomers" swaggertype:"array,object"`
DivisionsJSON json.RawMessage `gorm:"column:divisions_json;type:jsonb" json:"divisions" swaggertype:"array,object"`
HostJSON json.RawMessage `gorm:"column:host_json;type:jsonb" json:"host" swaggertype:"object"`
UID *string `gorm:"column:uid" json:"uid"`
TheSportsUpdatedAt *time.Time `gorm:"column:ts_updated_at" json:"theSportsUpdatedAt"`
Type string `gorm:"column:type;type:competition_type;default:league" json:"type"` Type string `gorm:"column:type;type:competition_type;default:league" json:"type"`
Category string `gorm:"column:category;default:default" json:"category"` Category string `gorm:"column:category;default:default" json:"category"`
IsActive bool `gorm:"column:is_active;default:true" json:"isActive"` IsActive bool `gorm:"column:is_active;default:true" json:"isActive"`
......
...@@ -54,6 +54,13 @@ func (s *coachService) ListCoaches(ctx context.Context, opts ListCoachesOptions) ...@@ -54,6 +54,13 @@ func (s *coachService) ListCoaches(ctx context.Context, opts ListCoachesOptions)
query = query.Where("is_active = ?", true) query = query.Where("is_active = ?", true)
} }
query = query.Order("is_active DESC")
query = query.Order("CASE WHEN current_team_wy_id IS NULL THEN 1 ELSE 0 END")
query = query.Order("CASE WHEN years_experience IS NULL THEN 1 ELSE 0 END")
query = query.Order("years_experience DESC")
query = query.Order("last_name ASC")
query = query.Order("first_name ASC")
var total int64 var total int64
if err := query.Count(&total).Error; err != nil { if err := query.Count(&total).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to count coaches", err) return nil, 0, errors.Wrap(errors.CodeInternal, "failed to count coaches", err)
......
...@@ -11,7 +11,7 @@ import ( ...@@ -11,7 +11,7 @@ import (
) )
type CompetitionService interface { type CompetitionService interface {
ListCompetitions(ctx context.Context, limit, offset int) ([]models.Competition, int64, error) ListCompetitions(ctx context.Context, limit, offset int, name, countryTsID *string) ([]models.Competition, int64, error)
GetByID(ctx context.Context, id string) (models.Competition, error) GetByID(ctx context.Context, id string) (models.Competition, error)
GetByWyID(ctx context.Context, wyID int) (models.Competition, error) GetByWyID(ctx context.Context, wyID int) (models.Competition, error)
GetByAnyProviderID(ctx context.Context, providerID string) (models.Competition, error) GetByAnyProviderID(ctx context.Context, providerID string) (models.Competition, error)
...@@ -25,9 +25,15 @@ func NewCompetitionService(db *gorm.DB) CompetitionService { ...@@ -25,9 +25,15 @@ func NewCompetitionService(db *gorm.DB) CompetitionService {
return &competitionService{db: db} return &competitionService{db: db}
} }
func (s *competitionService) ListCompetitions(ctx context.Context, limit, offset int) ([]models.Competition, int64, error) { func (s *competitionService) ListCompetitions(ctx context.Context, limit, offset int, name, countryTsID *string) ([]models.Competition, int64, error) {
var competitions []models.Competition var competitions []models.Competition
query := s.db.WithContext(ctx).Model(&models.Competition{}) query := s.db.WithContext(ctx).Model(&models.Competition{})
if name != nil && *name != "" {
query = query.Where("name ILIKE ?", "%"+*name+"%")
}
if countryTsID != nil && *countryTsID != "" {
query = query.Where("country_ts_id = ?", *countryTsID)
}
var total int64 var total int64
if err := query.Count(&total).Error; err != nil { if err := query.Count(&total).Error; err != nil {
......
...@@ -16,6 +16,7 @@ type MatchService interface { ...@@ -16,6 +16,7 @@ type MatchService interface {
GetByID(ctx context.Context, id string) (models.Match, error) GetByID(ctx context.Context, id string) (models.Match, error)
GetByTsID(ctx context.Context, matchTsID string) (models.Match, error) GetByTsID(ctx context.Context, matchTsID string) (models.Match, error)
GetHeadToHeadMostRecent(ctx context.Context, teamHomeID string, teamAwayID string, from *time.Time, to *time.Time) (models.Match, error) GetHeadToHeadMostRecent(ctx context.Context, teamHomeID string, teamAwayID string, from *time.Time, to *time.Time) (models.Match, error)
ListHeadToHead(ctx context.Context, teamHomeID string, teamAwayID string, from *time.Time, to *time.Time, limit int, offset int) ([]models.Match, int64, error)
GetLineup(ctx context.Context, matchTsID string) (models.Match, []models.MatchTeam, []models.MatchLineupPlayer, 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) GetLineupByID(ctx context.Context, id string) (models.Match, []models.MatchTeam, []models.MatchLineupPlayer, error)
} }
...@@ -27,6 +28,8 @@ type ListMatchesOptions struct { ...@@ -27,6 +28,8 @@ type ListMatchesOptions struct {
CompetitionWyID *int CompetitionWyID *int
SeasonWyID *int SeasonWyID *int
TeamTsID *string TeamTsID *string
TeamID *string
TeamSide *string // home|away|either
FromDate *time.Time FromDate *time.Time
ToDate *time.Time ToDate *time.Time
Status *string Status *string
...@@ -51,8 +54,40 @@ func (s *matchService) ListMatches(ctx context.Context, opts ListMatchesOptions) ...@@ -51,8 +54,40 @@ func (s *matchService) ListMatches(ctx context.Context, opts ListMatchesOptions)
if opts.SeasonWyID != nil { if opts.SeasonWyID != nil {
query = query.Where("season_wy_id = ?", *opts.SeasonWyID) query = query.Where("season_wy_id = ?", *opts.SeasonWyID)
} }
if opts.TeamTsID != nil && *opts.TeamTsID != "" { teamSide := "either"
query = query.Where("home_team_ts_id = ? OR away_team_ts_id = ?", *opts.TeamTsID, *opts.TeamTsID) if opts.TeamSide != nil && *opts.TeamSide != "" {
teamSide = *opts.TeamSide
}
if teamSide != "home" && teamSide != "away" && teamSide != "either" {
return nil, 0, errors.New(errors.CodeInvalidInput, "invalid teamSide (use home, away, or either)")
}
if opts.TeamID != nil && *opts.TeamID != "" {
var team models.Team
if err := s.db.WithContext(ctx).First(&team, "id = ?", *opts.TeamID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, 0, errors.New(errors.CodeNotFound, "team not found")
}
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to fetch team", err)
}
if team.TsID == "" {
return nil, 0, errors.New(errors.CodeNotFound, "team ts_id not found")
}
if teamSide == "home" {
query = query.Where("home_team_ts_id = ?", team.TsID)
} else if teamSide == "away" {
query = query.Where("away_team_ts_id = ?", team.TsID)
} else {
query = query.Where("home_team_ts_id = ? OR away_team_ts_id = ?", team.TsID, team.TsID)
}
} else if opts.TeamTsID != nil && *opts.TeamTsID != "" {
if teamSide == "home" {
query = query.Where("home_team_ts_id = ?", *opts.TeamTsID)
} else if teamSide == "away" {
query = query.Where("away_team_ts_id = ?", *opts.TeamTsID)
} else {
query = query.Where("home_team_ts_id = ? OR away_team_ts_id = ?", *opts.TeamTsID, *opts.TeamTsID)
}
} }
if opts.FromDate != nil { if opts.FromDate != nil {
query = query.Where("match_date >= ?", *opts.FromDate) query = query.Where("match_date >= ?", *opts.FromDate)
...@@ -146,6 +181,51 @@ func (s *matchService) GetHeadToHeadMostRecent(ctx context.Context, teamHomeID s ...@@ -146,6 +181,51 @@ func (s *matchService) GetHeadToHeadMostRecent(ctx context.Context, teamHomeID s
return match, nil return match, nil
} }
func (s *matchService) ListHeadToHead(ctx context.Context, teamHomeID string, teamAwayID string, from *time.Time, to *time.Time, limit int, offset int) ([]models.Match, int64, error) {
var matches []models.Match
var homeTeam models.Team
if err := s.db.WithContext(ctx).First(&homeTeam, "id = ?", teamHomeID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, 0, errors.New(errors.CodeNotFound, "home team not found")
}
return nil, 0, 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 nil, 0, errors.New(errors.CodeNotFound, "away team not found")
}
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to fetch away team", err)
}
if homeTeam.TsID == "" || awayTeam.TsID == "" {
return nil, 0, 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 = ?", homeTeam.TsID, awayTeam.TsID)
if from != nil {
q = q.Where("match_date >= ?", *from)
}
if to != nil {
q = q.Where("match_date <= ?", *to)
}
var total int64
if err := q.Count(&total).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to count head-to-head matches", err)
}
if err := q.Order("match_date desc").Limit(limit).Offset(offset).Find(&matches).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to fetch head-to-head matches", err)
}
return matches, total, nil
}
func (s *matchService) GetLineup(ctx context.Context, matchTsID string) (models.Match, []models.MatchTeam, []models.MatchLineupPlayer, error) { func (s *matchService) GetLineup(ctx context.Context, matchTsID string) (models.Match, []models.MatchTeam, []models.MatchLineupPlayer, error) {
match, err := s.GetByTsID(ctx, matchTsID) match, err := s.GetByTsID(ctx, matchTsID)
if err != nil { if err != nil {
......
...@@ -43,6 +43,12 @@ func (s *playerService) ListPlayers(ctx context.Context, opts ListPlayersOptions ...@@ -43,6 +43,12 @@ func (s *playerService) ListPlayers(ctx context.Context, opts ListPlayersOptions
"short_name ILIKE ? OR first_name ILIKE ? OR middle_name ILIKE ? OR last_name ILIKE ?", "short_name ILIKE ? OR first_name ILIKE ? OR middle_name ILIKE ? OR last_name ILIKE ?",
likePattern, likePattern, likePattern, likePattern, likePattern, likePattern, likePattern, likePattern,
) )
query = query.Order("CASE WHEN market_value IS NULL THEN 1 ELSE 0 END")
query = query.Order("market_value DESC")
query = query.Order("CASE WHEN current_national_team_id IS NULL THEN 1 ELSE 0 END")
query = query.Order("CASE WHEN current_team_id IS NULL AND (team_ts_id IS NULL OR team_ts_id = '') THEN 1 ELSE 0 END")
query = query.Order("last_name ASC")
query = query.Order("first_name ASC")
} else if opts.TeamID != "" { } else if opts.TeamID != "" {
query = query.Where("current_team_id = ?", opts.TeamID) query = query.Where("current_team_id = ?", opts.TeamID)
} else if opts.Country != "" { } else if opts.Country != "" {
......
...@@ -33,6 +33,13 @@ func (s *teamService) ListTeams(ctx context.Context, limit, offset int, name str ...@@ -33,6 +33,13 @@ func (s *teamService) ListTeams(ctx context.Context, limit, offset int, name str
query = query.Where("name ILIKE ?", "%"+name+"%") query = query.Where("name ILIKE ?", "%"+name+"%")
} }
query = query.Order("CASE WHEN market_value IS NULL THEN 1 ELSE 0 END")
query = query.Order("market_value DESC")
query = query.Order("is_active DESC")
query = query.Order("CASE WHEN competition_ts_id IS NULL OR competition_ts_id = '' THEN 1 ELSE 0 END")
query = query.Order("CASE WHEN league IS NULL OR league = '' THEN 1 ELSE 0 END")
query = query.Order("name ASC")
var total int64 var total int64
if err := query.Count(&total).Error; err != nil { if err := query.Count(&total).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to count teams", err) return nil, 0, errors.Wrap(errors.CodeInternal, "failed to count teams", err)
......
ALTER TABLE competitions
ADD COLUMN IF NOT EXISTS ts_category_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS country_ts_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS short_name TEXT,
ADD COLUMN IF NOT EXISTS logo TEXT,
ADD COLUMN IF NOT EXISTS ts_type INTEGER,
ADD COLUMN IF NOT EXISTS cur_season_ts_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS cur_stage_ts_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS cur_round INTEGER,
ADD COLUMN IF NOT EXISTS round_count INTEGER,
ADD COLUMN IF NOT EXISTS title_holder_json JSONB,
ADD COLUMN IF NOT EXISTS most_titles_json JSONB,
ADD COLUMN IF NOT EXISTS newcomers_json JSONB,
ADD COLUMN IF NOT EXISTS divisions_json JSONB,
ADD COLUMN IF NOT EXISTS host_json JSONB,
ADD COLUMN IF NOT EXISTS uid TEXT,
ADD COLUMN IF NOT EXISTS ts_updated_at TIMESTAMPTZ;
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