Commit c2aacc7f by Augusto

new migrations

parent 90a1bdcf
......@@ -28,14 +28,6 @@ definitions:
additionalProperties: true
type: object
type: object
handlers.MatchResponse:
properties:
data:
$ref: '#/definitions/handlers.matchOut'
meta:
additionalProperties: true
type: object
type: object
handlers.StructuredCoach:
properties:
apiLastSyncedAt:
......@@ -117,6 +109,95 @@ definitions:
additionalProperties: true
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:
properties:
aggScore:
......@@ -587,12 +668,19 @@ paths:
in: query
name: offset
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:
"200":
description: OK
schema:
additionalProperties: true
type: object
$ref: '#/definitions/handlers.competitionsListResponse'
"500":
description: Internal Server Error
schema:
......@@ -615,8 +703,7 @@ paths:
"200":
description: OK
schema:
additionalProperties: true
type: object
$ref: '#/definitions/handlers.competitionGetResponse'
"404":
description: Not Found
schema:
......@@ -632,6 +719,36 @@ paths:
summary: Get competition by ID
tags:
- 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}:
get:
description: Returns a single competition by its Wyscout wy_id identifier.
......@@ -645,8 +762,7 @@ paths:
"200":
description: OK
schema:
additionalProperties: true
type: object
$ref: '#/definitions/handlers.competitionGetResponse'
"400":
description: Bad Request
schema:
......@@ -1002,7 +1118,7 @@ paths:
/matches:
get:
description: Returns a paginated list of matches, optionally filtered by competitionWyId,
seasonWyId, teamTsId and date range.
seasonWyId, teamTsId/teamId (+teamSide) and date range.
parameters:
- description: Maximum number of items to return (default 100)
in: query
......@@ -1025,6 +1141,15 @@ paths:
in: query
name: teamTsId
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)
in: query
name: from
......@@ -1094,8 +1219,8 @@ paths:
- Matches
/matches/head-to-head:
get:
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).
parameters:
- description: Home team internal id
in: query
......@@ -1107,6 +1232,15 @@ paths:
name: teamAway
required: true
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)
in: query
name: from
......@@ -1119,7 +1253,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.MatchResponse'
$ref: '#/definitions/handlers.MatchListResponse'
"400":
description: Bad Request
schema:
......@@ -1138,7 +1272,7 @@ paths:
additionalProperties:
type: string
type: object
summary: Most recent match between two teams
summary: List matches between two teams
tags:
- Matches
/matches/id/{id}/lineup:
......
......@@ -9,16 +9,52 @@ import (
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/models"
"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 {
Service services.CompetitionService
DB *gorm.DB
}
func RegisterCompetitionRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewCompetitionService(db)
h := &CompetitionHandler{Service: service}
h := &CompetitionHandler{Service: service, DB: db}
competitions := rg.Group("/competitions")
competitions.GET("", h.List)
......@@ -33,12 +69,16 @@ func RegisterCompetitionRoutes(rg *gin.RouterGroup, db *gorm.DB) {
// @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{}
// @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
// @Router /competitions [get]
func (h *CompetitionHandler) List(c *gin.Context) {
limitStr := c.DefaultQuery("limit", "100")
offsetStr := c.DefaultQuery("offset", "0")
nameStr := c.Query("name")
countryIDStr := c.Query("countryId")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
......@@ -49,18 +89,52 @@ func (h *CompetitionHandler) List(c *gin.Context) {
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 {
respondError(c, err)
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
hasMore := int64(offset+limit) < total
timestamp := time.Now().UTC().Format(time.RFC3339)
c.JSON(http.StatusOK, gin.H{
"data": competitions,
"data": data,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": "/competitions",
......@@ -78,7 +152,7 @@ func (h *CompetitionHandler) List(c *gin.Context) {
// @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{}
// @Success 200 {object} competitionGetResponse
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /competitions/{id} [get]
......@@ -91,11 +165,20 @@ func (h *CompetitionHandler) GetByID(c *gin.Context) {
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)
endpoint := fmt.Sprintf("/competitions/%s", id)
c.JSON(http.StatusOK, gin.H{
"data": comp,
"data": competitionResponse{Competition: comp, CountryName: countryName},
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
......@@ -105,6 +188,14 @@ func (h *CompetitionHandler) GetByID(c *gin.Context) {
}
// 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) {
providerID := c.Param("providerId")
......@@ -114,11 +205,20 @@ func (h *CompetitionHandler) GetByProviderID(c *gin.Context) {
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)
endpoint := fmt.Sprintf("/competitions/provider/%s", providerID)
c.JSON(http.StatusOK, gin.H{
"data": comp,
"data": competitionResponse{Competition: comp, CountryName: countryName},
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
......@@ -132,7 +232,7 @@ func (h *CompetitionHandler) GetByProviderID(c *gin.Context) {
// @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{}
// @Success 200 {object} competitionGetResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
......@@ -151,11 +251,20 @@ func (h *CompetitionHandler) GetByWyID(c *gin.Context) {
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)
endpoint := fmt.Sprintf("/competitions/wyscout/%d", wyID)
c.JSON(http.StatusOK, gin.H{
"data": comp,
"data": competitionResponse{Competition: comp, CountryName: countryName},
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
......
......@@ -1306,6 +1306,14 @@ func (h *ImportHandler) ImportCompetitions(c *gin.Context) {
Logo *string `json:"logo"`
Type int `json:"type"`
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"`
UID *string `json:"uid"`
UpdatedAt *int64 `json:"updated_at"`
......@@ -1330,6 +1338,43 @@ func (h *ImportHandler) ImportCompetitions(c *gin.Context) {
TsID: r.ID,
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
if r.Gender != nil {
gender := ""
......@@ -1365,6 +1410,57 @@ func (h *ImportHandler) ImportCompetitions(c *gin.Context) {
// update existing competition
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 {
gender := ""
switch *r.Gender {
......
......@@ -333,13 +333,15 @@ func (h *MatchHandler) GetLineupByMatchID(c *gin.Context) {
// 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
// @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 competitionWyId query int false "Filter by competition 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 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 to query string false "Filter matches on/before date (RFC3339 or YYYY-MM-DD)"
// @Param status query string false "Filter by match status"
......@@ -386,6 +388,21 @@ func (h *MatchHandler) List(c *gin.Context) {
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) {
if raw == "" {
return nil, nil
......@@ -429,6 +446,8 @@ func (h *MatchHandler) List(c *gin.Context) {
CompetitionWyID: competitionWyID,
SeasonWyID: seasonWyID,
TeamTsID: teamTsID,
TeamID: teamID,
TeamSide: teamSide,
FromDate: from,
ToDate: to,
Status: status,
......@@ -468,14 +487,16 @@ func (h *MatchHandler) List(c *gin.Context) {
}
// HeadToHeadMostRecent
// @Summary Most recent match between two teams
// @Description Returns the most recent match where teamHome played at home and teamAway played away (by internal team id).
// @Summary List matches between two teams
// @Description Returns a paginated list of matches where teamHome played at home and teamAway played away (by internal team id).
// @Tags Matches
// @Param teamHome query string true "Home 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 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 404 {object} map[string]string
// @Failure 500 {object} map[string]string
......@@ -483,6 +504,17 @@ func (h *MatchHandler) List(c *gin.Context) {
func (h *MatchHandler) HeadToHeadMostRecent(c *gin.Context) {
teamHome := c.Query("teamHome")
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) {
if raw == "" {
......@@ -524,20 +556,22 @@ func (h *MatchHandler) HeadToHeadMostRecent(c *gin.Context) {
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 {
respondError(c, err)
return
}
matchWithNames, err := h.enrichMatch(c.Request.Context(), match)
matchesWithNames, err := h.enrichMatches(c.Request.Context(), matches)
if err != nil {
respondError(c, err)
return
}
page := offset/limit + 1
hasMore := int64(offset+limit) < total
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 {
endpoint = fmt.Sprintf("%s&from=%s", endpoint, from.UTC().Format(time.RFC3339))
}
......@@ -546,11 +580,15 @@ func (h *MatchHandler) HeadToHeadMostRecent(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{
"data": matchWithNames,
"data": matchesWithNames,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
"totalItems": total,
"page": page,
"limit": limit,
"hasMore": hasMore,
},
})
}
......
......@@ -43,10 +43,26 @@ type Competition struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
WyID *int `gorm:"column:wy_id" json:"wyId"`
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"`
ShortName *string `gorm:"column:short_name" json:"shortName"`
Logo *string `gorm:"column:logo" json:"logo"`
OfficialName *string `gorm:"column:official_name" json:"officialName"`
AreaWyID *int `gorm:"column:area_wy_id" json:"areaWyId"`
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"`
Category string `gorm:"column:category;default:default" json:"category"`
IsActive bool `gorm:"column:is_active;default:true" json:"isActive"`
......
......@@ -54,6 +54,13 @@ func (s *coachService) ListCoaches(ctx context.Context, opts ListCoachesOptions)
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
if err := query.Count(&total).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to count coaches", err)
......
......@@ -11,7 +11,7 @@ import (
)
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)
GetByWyID(ctx context.Context, wyID int) (models.Competition, error)
GetByAnyProviderID(ctx context.Context, providerID string) (models.Competition, error)
......@@ -25,9 +25,15 @@ func NewCompetitionService(db *gorm.DB) CompetitionService {
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
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
if err := query.Count(&total).Error; err != nil {
......
......@@ -16,6 +16,7 @@ type MatchService interface {
GetByID(ctx context.Context, id 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)
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)
GetLineupByID(ctx context.Context, id string) (models.Match, []models.MatchTeam, []models.MatchLineupPlayer, error)
}
......@@ -27,6 +28,8 @@ type ListMatchesOptions struct {
CompetitionWyID *int
SeasonWyID *int
TeamTsID *string
TeamID *string
TeamSide *string // home|away|either
FromDate *time.Time
ToDate *time.Time
Status *string
......@@ -51,8 +54,40 @@ func (s *matchService) ListMatches(ctx context.Context, opts ListMatchesOptions)
if opts.SeasonWyID != nil {
query = query.Where("season_wy_id = ?", *opts.SeasonWyID)
}
if opts.TeamTsID != nil && *opts.TeamTsID != "" {
query = query.Where("home_team_ts_id = ? OR away_team_ts_id = ?", *opts.TeamTsID, *opts.TeamTsID)
teamSide := "either"
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 {
query = query.Where("match_date >= ?", *opts.FromDate)
......@@ -146,6 +181,51 @@ func (s *matchService) GetHeadToHeadMostRecent(ctx context.Context, teamHomeID s
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) {
match, err := s.GetByTsID(ctx, matchTsID)
if err != nil {
......
......@@ -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 ?",
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 != "" {
query = query.Where("current_team_id = ?", opts.TeamID)
} else if opts.Country != "" {
......
......@@ -33,6 +33,13 @@ func (s *teamService) ListTeams(ctx context.Context, limit, offset int, name str
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
if err := query.Count(&total).Error; err != nil {
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