Commit 90a1bdcf by Augusto

new data and endpoint fix

parent 383db895
...@@ -213,8 +213,7 @@ const docTemplate = `{ ...@@ -213,8 +213,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.CoachListResponse"
"additionalProperties": true
} }
}, },
"500": { "500": {
...@@ -249,8 +248,7 @@ const docTemplate = `{ ...@@ -249,8 +248,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.CoachResponse"
"additionalProperties": true
} }
}, },
"404": { "404": {
...@@ -294,8 +292,7 @@ const docTemplate = `{ ...@@ -294,8 +292,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.CoachResponse"
"additionalProperties": true
} }
}, },
"400": { "400": {
...@@ -348,6 +345,145 @@ const docTemplate = `{ ...@@ -348,6 +345,145 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.CoachResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/competitions": {
"get": {
"description": "Returns a list of competitions with optional pagination.",
"tags": [
"Competitions"
],
"summary": "List competitions",
"parameters": [
{
"type": "integer",
"description": "Maximum number of items to return (default 100)",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Number of items to skip before starting to collect the result set (default 0)",
"name": "offset",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/competitions/wyscout/{wyId}": {
"get": {
"description": "Returns a single competition by its Wyscout wy_id identifier.",
"tags": [
"Competitions"
],
"summary": "Get competition by Wyscout ID",
"parameters": [
{
"type": "integer",
"description": "Wyscout wy_id identifier",
"name": "wyId",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/competitions/{id}": {
"get": {
"description": "Returns a single competition by its internal ID.",
"tags": [
"Competitions"
],
"summary": "Get competition by ID",
"parameters": [
{
"type": "string",
"description": "Competition internal identifier",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object", "type": "object",
"additionalProperties": true "additionalProperties": true
} }
...@@ -880,8 +1016,7 @@ const docTemplate = `{ ...@@ -880,8 +1016,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.MatchListResponse"
"additionalProperties": true
} }
}, },
"400": { "400": {
...@@ -907,7 +1042,7 @@ const docTemplate = `{ ...@@ -907,7 +1042,7 @@ const docTemplate = `{
}, },
"/matches/head-to-head": { "/matches/head-to-head": {
"get": { "get": {
"description": "Returns the most recent match between teamA and teamB (by ts_id), regardless of home/away.", "description": "Returns the most recent match where teamHome played at home and teamAway played away (by internal team id).",
"tags": [ "tags": [
"Matches" "Matches"
], ],
...@@ -915,25 +1050,36 @@ const docTemplate = `{ ...@@ -915,25 +1050,36 @@ const docTemplate = `{
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Team A ts_id", "description": "Home team internal id",
"name": "teamA", "name": "teamHome",
"in": "query", "in": "query",
"required": true "required": true
}, },
{ {
"type": "string", "type": "string",
"description": "Team B ts_id", "description": "Away team internal id",
"name": "teamB", "name": "teamAway",
"in": "query", "in": "query",
"required": true "required": true
},
{
"type": "string",
"description": "Filter matches on/after date (RFC3339 or YYYY-MM-DD)",
"name": "from",
"in": "query"
},
{
"type": "string",
"description": "Filter matches on/before date (RFC3339 or YYYY-MM-DD)",
"name": "to",
"in": "query"
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.MatchResponse"
"additionalProperties": true
} }
}, },
"400": { "400": {
...@@ -1423,13 +1569,13 @@ const docTemplate = `{ ...@@ -1423,13 +1569,13 @@ const docTemplate = `{
} }
} }
}, },
"/teams": { "/seasons": {
"get": { "get": {
"description": "Returns a list of teams with optional pagination.", "description": "Returns a list of seasons with optional pagination.",
"tags": [ "tags": [
"Teams" "Seasons"
], ],
"summary": "List teams", "summary": "List seasons",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "integer",
...@@ -1464,13 +1610,13 @@ const docTemplate = `{ ...@@ -1464,13 +1610,13 @@ const docTemplate = `{
} }
} }
}, },
"/teams/wyscout/{wyId}": { "/seasons/wyscout/{wyId}": {
"get": { "get": {
"description": "Returns a single team by its Wyscout wy_id identifier.", "description": "Returns a single season by its Wyscout wy_id identifier.",
"tags": [ "tags": [
"Teams" "Seasons"
], ],
"summary": "Get team by Wyscout ID", "summary": "Get season by Wyscout ID",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "integer",
...@@ -1518,17 +1664,17 @@ const docTemplate = `{ ...@@ -1518,17 +1664,17 @@ const docTemplate = `{
} }
} }
}, },
"/teams/{id}": { "/seasons/{id}": {
"get": { "get": {
"description": "Returns a single team by its internal ID.", "description": "Returns a single season by its internal ID.",
"tags": [ "tags": [
"Teams" "Seasons"
], ],
"summary": "Get team by ID", "summary": "Get season by ID",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Team internal identifier", "description": "Season internal identifier",
"name": "id", "name": "id",
"in": "path", "in": "path",
"required": true "required": true
...@@ -1562,6 +1708,640 @@ const docTemplate = `{ ...@@ -1562,6 +1708,640 @@ const docTemplate = `{
} }
} }
} }
},
"/teams": {
"get": {
"description": "Returns a list of teams with optional pagination.",
"tags": [
"Teams"
],
"summary": "List teams",
"parameters": [
{
"type": "integer",
"description": "Maximum number of items to return (default 100)",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Number of items to skip before starting to collect the result set (default 0)",
"name": "offset",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.TeamListResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/teams/wyscout/{wyId}": {
"get": {
"description": "Returns a single team by its Wyscout wy_id identifier.",
"tags": [
"Teams"
],
"summary": "Get team by Wyscout ID",
"parameters": [
{
"type": "integer",
"description": "Wyscout wy_id identifier",
"name": "wyId",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.TeamResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/teams/{id}": {
"get": {
"description": "Returns a single team by its internal ID.",
"tags": [
"Teams"
],
"summary": "Get team by ID",
"parameters": [
{
"type": "string",
"description": "Team internal identifier",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.TeamResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
}
},
"definitions": {
"handlers.CoachListResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.StructuredCoach"
}
},
"meta": {
"type": "object",
"additionalProperties": true
}
}
},
"handlers.CoachResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/handlers.StructuredCoach"
},
"meta": {
"type": "object",
"additionalProperties": true
}
}
},
"handlers.MatchListResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.matchOut"
}
},
"meta": {
"type": "object",
"additionalProperties": true
}
}
},
"handlers.MatchResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/handlers.matchOut"
},
"meta": {
"type": "object",
"additionalProperties": true
}
}
},
"handlers.StructuredCoach": {
"type": "object",
"properties": {
"apiLastSyncedAt": {
"type": "string"
},
"apiSyncStatus": {
"type": "string"
},
"coachingLicense": {
"type": "string"
},
"contractUntil": {
"type": "string"
},
"countryName": {
"type": "string"
},
"countryTsId": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"currentTeamWyId": {
"type": "integer"
},
"dateOfBirth": {
"type": "string"
},
"deathday": {
"type": "string"
},
"deletedAt": {
"type": "string"
},
"firstName": {
"type": "string"
},
"id": {
"type": "string"
},
"imageDataUrl": {
"type": "string"
},
"isActive": {
"type": "boolean"
},
"joinedAt": {
"type": "string"
},
"lastName": {
"type": "string"
},
"middleName": {
"type": "string"
},
"nationalityWyId": {
"type": "integer"
},
"position": {
"type": "string"
},
"preferredFormation": {
"type": "string"
},
"shortName": {
"type": "string"
},
"status": {
"type": "string"
},
"teamName": {
"type": "string"
},
"teamTsId": {
"type": "string"
},
"tsId": {
"type": "string"
},
"uid": {
"type": "string"
},
"updatedAt": {
"type": "string"
},
"wyId": {
"type": "integer"
},
"yearsExperience": {
"type": "integer"
}
}
},
"handlers.TeamListResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Team"
}
},
"meta": {
"type": "object",
"additionalProperties": true
}
}
},
"handlers.TeamResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.Team"
},
"meta": {
"type": "object",
"additionalProperties": true
}
}
},
"handlers.matchOut": {
"type": "object",
"properties": {
"aggScore": {
"type": "object"
},
"apiLastSyncedAt": {
"type": "string"
},
"apiSyncStatus": {
"type": "string"
},
"assistantReferee1WyId": {
"type": "integer"
},
"assistantReferee2WyId": {
"type": "integer"
},
"attendance": {
"type": "integer"
},
"awayPosition": {
"type": "string"
},
"awayScore": {
"type": "integer"
},
"awayScorePenalties": {
"type": "integer"
},
"awayScores": {
"type": "object"
},
"awayTeamId": {
"type": "string"
},
"awayTeamName": {
"type": "string"
},
"awayTeamTsId": {
"type": "string"
},
"awayTeamWyId": {
"type": "integer"
},
"competitionName": {
"type": "string"
},
"competitionTsId": {
"type": "string"
},
"competitionWyId": {
"type": "integer"
},
"coverageLineup": {
"type": "boolean"
},
"coverageMlive": {
"type": "boolean"
},
"createdAt": {
"type": "string"
},
"deletedAt": {
"type": "string"
},
"endedUnix": {
"type": "integer"
},
"fourthRefereeWyId": {
"type": "integer"
},
"hasOt": {
"type": "boolean"
},
"homePosition": {
"type": "string"
},
"homeScore": {
"type": "integer"
},
"homeScorePenalties": {
"type": "integer"
},
"homeScores": {
"type": "object"
},
"homeTeamId": {
"type": "string"
},
"homeTeamName": {
"type": "string"
},
"homeTeamTsId": {
"type": "string"
},
"homeTeamWyId": {
"type": "integer"
},
"humidity": {
"type": "string"
},
"id": {
"type": "string"
},
"loss": {
"type": "boolean"
},
"mainRefereeWyId": {
"type": "integer"
},
"matchDate": {
"type": "string"
},
"matchTimeUnix": {
"type": "integer"
},
"matchType": {
"type": "string"
},
"neutral": {
"type": "boolean"
},
"notes": {
"type": "string"
},
"pressure": {
"type": "string"
},
"providerUpdatedAtUnix": {
"type": "integer"
},
"refereeTsId": {
"type": "string"
},
"relatedTsId": {
"type": "string"
},
"roundGroupNum": {
"type": "integer"
},
"roundNum": {
"type": "integer"
},
"roundStageTsId": {
"type": "string"
},
"roundWyId": {
"type": "integer"
},
"seasonName": {
"type": "string"
},
"seasonTsId": {
"type": "string"
},
"seasonWyId": {
"type": "integer"
},
"status": {
"type": "string"
},
"statusId": {
"type": "integer"
},
"tbd": {
"type": "boolean"
},
"teamReverse": {
"type": "boolean"
},
"temperature": {
"type": "number"
},
"tsId": {
"type": "string"
},
"updatedAt": {
"type": "string"
},
"varRefereeWyId": {
"type": "integer"
},
"venue": {
"type": "string"
},
"venueCity": {
"type": "string"
},
"venueCountry": {
"type": "string"
},
"venueTsId": {
"type": "string"
},
"weather": {
"type": "string"
},
"wind": {
"type": "string"
},
"wyId": {
"type": "integer"
}
}
},
"models.Team": {
"type": "object",
"properties": {
"apiLastSyncedAt": {
"type": "string"
},
"apiSyncStatus": {
"type": "string"
},
"areaWyId": {
"type": "integer"
},
"category": {
"type": "string"
},
"city": {
"type": "string"
},
"coachTsId": {
"type": "string"
},
"coachWyId": {
"type": "integer"
},
"competitionTsId": {
"type": "string"
},
"competitionWyId": {
"type": "integer"
},
"countryTsId": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"deletedAt": {
"type": "string"
},
"description": {
"type": "string"
},
"foreignPlayers": {
"type": "integer"
},
"gender": {
"type": "string"
},
"gsmId": {
"type": "integer"
},
"id": {
"type": "string"
},
"imageDataUrl": {
"type": "string"
},
"isActive": {
"type": "boolean"
},
"league": {
"type": "string"
},
"marketValue": {
"type": "integer"
},
"marketValueCurrency": {
"type": "string"
},
"name": {
"type": "string"
},
"nationalPlayers": {
"type": "integer"
},
"officialName": {
"type": "string"
},
"season": {
"type": "string"
},
"seasonWyId": {
"type": "integer"
},
"shortName": {
"type": "string"
},
"status": {
"type": "string"
},
"totalPlayers": {
"type": "integer"
},
"tsId": {
"type": "string"
},
"type": {
"type": "string"
},
"updatedAt": {
"type": "string"
},
"venueTsId": {
"type": "string"
},
"website": {
"type": "string"
},
"wyId": {
"type": "integer"
}
}
} }
} }
}` }`
......
...@@ -206,8 +206,7 @@ ...@@ -206,8 +206,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.CoachListResponse"
"additionalProperties": true
} }
}, },
"500": { "500": {
...@@ -242,8 +241,7 @@ ...@@ -242,8 +241,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.CoachResponse"
"additionalProperties": true
} }
}, },
"404": { "404": {
...@@ -287,8 +285,7 @@ ...@@ -287,8 +285,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.CoachResponse"
"additionalProperties": true
} }
}, },
"400": { "400": {
...@@ -341,6 +338,145 @@ ...@@ -341,6 +338,145 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.CoachResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/competitions": {
"get": {
"description": "Returns a list of competitions with optional pagination.",
"tags": [
"Competitions"
],
"summary": "List competitions",
"parameters": [
{
"type": "integer",
"description": "Maximum number of items to return (default 100)",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Number of items to skip before starting to collect the result set (default 0)",
"name": "offset",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/competitions/wyscout/{wyId}": {
"get": {
"description": "Returns a single competition by its Wyscout wy_id identifier.",
"tags": [
"Competitions"
],
"summary": "Get competition by Wyscout ID",
"parameters": [
{
"type": "integer",
"description": "Wyscout wy_id identifier",
"name": "wyId",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/competitions/{id}": {
"get": {
"description": "Returns a single competition by its internal ID.",
"tags": [
"Competitions"
],
"summary": "Get competition by ID",
"parameters": [
{
"type": "string",
"description": "Competition internal identifier",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object", "type": "object",
"additionalProperties": true "additionalProperties": true
} }
...@@ -873,8 +1009,7 @@ ...@@ -873,8 +1009,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.MatchListResponse"
"additionalProperties": true
} }
}, },
"400": { "400": {
...@@ -900,7 +1035,7 @@ ...@@ -900,7 +1035,7 @@
}, },
"/matches/head-to-head": { "/matches/head-to-head": {
"get": { "get": {
"description": "Returns the most recent match between teamA and teamB (by ts_id), regardless of home/away.", "description": "Returns the most recent match where teamHome played at home and teamAway played away (by internal team id).",
"tags": [ "tags": [
"Matches" "Matches"
], ],
...@@ -908,25 +1043,36 @@ ...@@ -908,25 +1043,36 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Team A ts_id", "description": "Home team internal id",
"name": "teamA", "name": "teamHome",
"in": "query", "in": "query",
"required": true "required": true
}, },
{ {
"type": "string", "type": "string",
"description": "Team B ts_id", "description": "Away team internal id",
"name": "teamB", "name": "teamAway",
"in": "query", "in": "query",
"required": true "required": true
},
{
"type": "string",
"description": "Filter matches on/after date (RFC3339 or YYYY-MM-DD)",
"name": "from",
"in": "query"
},
{
"type": "string",
"description": "Filter matches on/before date (RFC3339 or YYYY-MM-DD)",
"name": "to",
"in": "query"
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.MatchResponse"
"additionalProperties": true
} }
}, },
"400": { "400": {
...@@ -1416,13 +1562,13 @@ ...@@ -1416,13 +1562,13 @@
} }
} }
}, },
"/teams": { "/seasons": {
"get": { "get": {
"description": "Returns a list of teams with optional pagination.", "description": "Returns a list of seasons with optional pagination.",
"tags": [ "tags": [
"Teams" "Seasons"
], ],
"summary": "List teams", "summary": "List seasons",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "integer",
...@@ -1457,13 +1603,13 @@ ...@@ -1457,13 +1603,13 @@
} }
} }
}, },
"/teams/wyscout/{wyId}": { "/seasons/wyscout/{wyId}": {
"get": { "get": {
"description": "Returns a single team by its Wyscout wy_id identifier.", "description": "Returns a single season by its Wyscout wy_id identifier.",
"tags": [ "tags": [
"Teams" "Seasons"
], ],
"summary": "Get team by Wyscout ID", "summary": "Get season by Wyscout ID",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "integer",
...@@ -1511,17 +1657,17 @@ ...@@ -1511,17 +1657,17 @@
} }
} }
}, },
"/teams/{id}": { "/seasons/{id}": {
"get": { "get": {
"description": "Returns a single team by its internal ID.", "description": "Returns a single season by its internal ID.",
"tags": [ "tags": [
"Teams" "Seasons"
], ],
"summary": "Get team by ID", "summary": "Get season by ID",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Team internal identifier", "description": "Season internal identifier",
"name": "id", "name": "id",
"in": "path", "in": "path",
"required": true "required": true
...@@ -1555,6 +1701,640 @@ ...@@ -1555,6 +1701,640 @@
} }
} }
} }
},
"/teams": {
"get": {
"description": "Returns a list of teams with optional pagination.",
"tags": [
"Teams"
],
"summary": "List teams",
"parameters": [
{
"type": "integer",
"description": "Maximum number of items to return (default 100)",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Number of items to skip before starting to collect the result set (default 0)",
"name": "offset",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.TeamListResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/teams/wyscout/{wyId}": {
"get": {
"description": "Returns a single team by its Wyscout wy_id identifier.",
"tags": [
"Teams"
],
"summary": "Get team by Wyscout ID",
"parameters": [
{
"type": "integer",
"description": "Wyscout wy_id identifier",
"name": "wyId",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.TeamResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/teams/{id}": {
"get": {
"description": "Returns a single team by its internal ID.",
"tags": [
"Teams"
],
"summary": "Get team by ID",
"parameters": [
{
"type": "string",
"description": "Team internal identifier",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.TeamResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
}
},
"definitions": {
"handlers.CoachListResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.StructuredCoach"
}
},
"meta": {
"type": "object",
"additionalProperties": true
}
}
},
"handlers.CoachResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/handlers.StructuredCoach"
},
"meta": {
"type": "object",
"additionalProperties": true
}
}
},
"handlers.MatchListResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.matchOut"
}
},
"meta": {
"type": "object",
"additionalProperties": true
}
}
},
"handlers.MatchResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/handlers.matchOut"
},
"meta": {
"type": "object",
"additionalProperties": true
}
}
},
"handlers.StructuredCoach": {
"type": "object",
"properties": {
"apiLastSyncedAt": {
"type": "string"
},
"apiSyncStatus": {
"type": "string"
},
"coachingLicense": {
"type": "string"
},
"contractUntil": {
"type": "string"
},
"countryName": {
"type": "string"
},
"countryTsId": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"currentTeamWyId": {
"type": "integer"
},
"dateOfBirth": {
"type": "string"
},
"deathday": {
"type": "string"
},
"deletedAt": {
"type": "string"
},
"firstName": {
"type": "string"
},
"id": {
"type": "string"
},
"imageDataUrl": {
"type": "string"
},
"isActive": {
"type": "boolean"
},
"joinedAt": {
"type": "string"
},
"lastName": {
"type": "string"
},
"middleName": {
"type": "string"
},
"nationalityWyId": {
"type": "integer"
},
"position": {
"type": "string"
},
"preferredFormation": {
"type": "string"
},
"shortName": {
"type": "string"
},
"status": {
"type": "string"
},
"teamName": {
"type": "string"
},
"teamTsId": {
"type": "string"
},
"tsId": {
"type": "string"
},
"uid": {
"type": "string"
},
"updatedAt": {
"type": "string"
},
"wyId": {
"type": "integer"
},
"yearsExperience": {
"type": "integer"
}
}
},
"handlers.TeamListResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Team"
}
},
"meta": {
"type": "object",
"additionalProperties": true
}
}
},
"handlers.TeamResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.Team"
},
"meta": {
"type": "object",
"additionalProperties": true
}
}
},
"handlers.matchOut": {
"type": "object",
"properties": {
"aggScore": {
"type": "object"
},
"apiLastSyncedAt": {
"type": "string"
},
"apiSyncStatus": {
"type": "string"
},
"assistantReferee1WyId": {
"type": "integer"
},
"assistantReferee2WyId": {
"type": "integer"
},
"attendance": {
"type": "integer"
},
"awayPosition": {
"type": "string"
},
"awayScore": {
"type": "integer"
},
"awayScorePenalties": {
"type": "integer"
},
"awayScores": {
"type": "object"
},
"awayTeamId": {
"type": "string"
},
"awayTeamName": {
"type": "string"
},
"awayTeamTsId": {
"type": "string"
},
"awayTeamWyId": {
"type": "integer"
},
"competitionName": {
"type": "string"
},
"competitionTsId": {
"type": "string"
},
"competitionWyId": {
"type": "integer"
},
"coverageLineup": {
"type": "boolean"
},
"coverageMlive": {
"type": "boolean"
},
"createdAt": {
"type": "string"
},
"deletedAt": {
"type": "string"
},
"endedUnix": {
"type": "integer"
},
"fourthRefereeWyId": {
"type": "integer"
},
"hasOt": {
"type": "boolean"
},
"homePosition": {
"type": "string"
},
"homeScore": {
"type": "integer"
},
"homeScorePenalties": {
"type": "integer"
},
"homeScores": {
"type": "object"
},
"homeTeamId": {
"type": "string"
},
"homeTeamName": {
"type": "string"
},
"homeTeamTsId": {
"type": "string"
},
"homeTeamWyId": {
"type": "integer"
},
"humidity": {
"type": "string"
},
"id": {
"type": "string"
},
"loss": {
"type": "boolean"
},
"mainRefereeWyId": {
"type": "integer"
},
"matchDate": {
"type": "string"
},
"matchTimeUnix": {
"type": "integer"
},
"matchType": {
"type": "string"
},
"neutral": {
"type": "boolean"
},
"notes": {
"type": "string"
},
"pressure": {
"type": "string"
},
"providerUpdatedAtUnix": {
"type": "integer"
},
"refereeTsId": {
"type": "string"
},
"relatedTsId": {
"type": "string"
},
"roundGroupNum": {
"type": "integer"
},
"roundNum": {
"type": "integer"
},
"roundStageTsId": {
"type": "string"
},
"roundWyId": {
"type": "integer"
},
"seasonName": {
"type": "string"
},
"seasonTsId": {
"type": "string"
},
"seasonWyId": {
"type": "integer"
},
"status": {
"type": "string"
},
"statusId": {
"type": "integer"
},
"tbd": {
"type": "boolean"
},
"teamReverse": {
"type": "boolean"
},
"temperature": {
"type": "number"
},
"tsId": {
"type": "string"
},
"updatedAt": {
"type": "string"
},
"varRefereeWyId": {
"type": "integer"
},
"venue": {
"type": "string"
},
"venueCity": {
"type": "string"
},
"venueCountry": {
"type": "string"
},
"venueTsId": {
"type": "string"
},
"weather": {
"type": "string"
},
"wind": {
"type": "string"
},
"wyId": {
"type": "integer"
}
}
},
"models.Team": {
"type": "object",
"properties": {
"apiLastSyncedAt": {
"type": "string"
},
"apiSyncStatus": {
"type": "string"
},
"areaWyId": {
"type": "integer"
},
"category": {
"type": "string"
},
"city": {
"type": "string"
},
"coachTsId": {
"type": "string"
},
"coachWyId": {
"type": "integer"
},
"competitionTsId": {
"type": "string"
},
"competitionWyId": {
"type": "integer"
},
"countryTsId": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"deletedAt": {
"type": "string"
},
"description": {
"type": "string"
},
"foreignPlayers": {
"type": "integer"
},
"gender": {
"type": "string"
},
"gsmId": {
"type": "integer"
},
"id": {
"type": "string"
},
"imageDataUrl": {
"type": "string"
},
"isActive": {
"type": "boolean"
},
"league": {
"type": "string"
},
"marketValue": {
"type": "integer"
},
"marketValueCurrency": {
"type": "string"
},
"name": {
"type": "string"
},
"nationalPlayers": {
"type": "integer"
},
"officialName": {
"type": "string"
},
"season": {
"type": "string"
},
"seasonWyId": {
"type": "integer"
},
"shortName": {
"type": "string"
},
"status": {
"type": "string"
},
"totalPlayers": {
"type": "integer"
},
"tsId": {
"type": "string"
},
"type": {
"type": "string"
},
"updatedAt": {
"type": "string"
},
"venueTsId": {
"type": "string"
},
"website": {
"type": "string"
},
"wyId": {
"type": "integer"
}
}
} }
} }
} }
\ No newline at end of file
basePath: /api basePath: /api
definitions:
handlers.CoachListResponse:
properties:
data:
items:
$ref: '#/definitions/handlers.StructuredCoach'
type: array
meta:
additionalProperties: true
type: object
type: object
handlers.CoachResponse:
properties:
data:
$ref: '#/definitions/handlers.StructuredCoach'
meta:
additionalProperties: true
type: object
type: object
handlers.MatchListResponse:
properties:
data:
items:
$ref: '#/definitions/handlers.matchOut'
type: array
meta:
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:
type: string
apiSyncStatus:
type: string
coachingLicense:
type: string
contractUntil:
type: string
countryName:
type: string
countryTsId:
type: string
createdAt:
type: string
currentTeamWyId:
type: integer
dateOfBirth:
type: string
deathday:
type: string
deletedAt:
type: string
firstName:
type: string
id:
type: string
imageDataUrl:
type: string
isActive:
type: boolean
joinedAt:
type: string
lastName:
type: string
middleName:
type: string
nationalityWyId:
type: integer
position:
type: string
preferredFormation:
type: string
shortName:
type: string
status:
type: string
teamName:
type: string
teamTsId:
type: string
tsId:
type: string
uid:
type: string
updatedAt:
type: string
wyId:
type: integer
yearsExperience:
type: integer
type: object
handlers.TeamListResponse:
properties:
data:
items:
$ref: '#/definitions/models.Team'
type: array
meta:
additionalProperties: true
type: object
type: object
handlers.TeamResponse:
properties:
data:
$ref: '#/definitions/models.Team'
meta:
additionalProperties: true
type: object
type: object
handlers.matchOut:
properties:
aggScore:
type: object
apiLastSyncedAt:
type: string
apiSyncStatus:
type: string
assistantReferee1WyId:
type: integer
assistantReferee2WyId:
type: integer
attendance:
type: integer
awayPosition:
type: string
awayScore:
type: integer
awayScorePenalties:
type: integer
awayScores:
type: object
awayTeamId:
type: string
awayTeamName:
type: string
awayTeamTsId:
type: string
awayTeamWyId:
type: integer
competitionName:
type: string
competitionTsId:
type: string
competitionWyId:
type: integer
coverageLineup:
type: boolean
coverageMlive:
type: boolean
createdAt:
type: string
deletedAt:
type: string
endedUnix:
type: integer
fourthRefereeWyId:
type: integer
hasOt:
type: boolean
homePosition:
type: string
homeScore:
type: integer
homeScorePenalties:
type: integer
homeScores:
type: object
homeTeamId:
type: string
homeTeamName:
type: string
homeTeamTsId:
type: string
homeTeamWyId:
type: integer
humidity:
type: string
id:
type: string
loss:
type: boolean
mainRefereeWyId:
type: integer
matchDate:
type: string
matchTimeUnix:
type: integer
matchType:
type: string
neutral:
type: boolean
notes:
type: string
pressure:
type: string
providerUpdatedAtUnix:
type: integer
refereeTsId:
type: string
relatedTsId:
type: string
roundGroupNum:
type: integer
roundNum:
type: integer
roundStageTsId:
type: string
roundWyId:
type: integer
seasonName:
type: string
seasonTsId:
type: string
seasonWyId:
type: integer
status:
type: string
statusId:
type: integer
tbd:
type: boolean
teamReverse:
type: boolean
temperature:
type: number
tsId:
type: string
updatedAt:
type: string
varRefereeWyId:
type: integer
venue:
type: string
venueCity:
type: string
venueCountry:
type: string
venueTsId:
type: string
weather:
type: string
wind:
type: string
wyId:
type: integer
type: object
models.Team:
properties:
apiLastSyncedAt:
type: string
apiSyncStatus:
type: string
areaWyId:
type: integer
category:
type: string
city:
type: string
coachTsId:
type: string
coachWyId:
type: integer
competitionTsId:
type: string
competitionWyId:
type: integer
countryTsId:
type: string
createdAt:
type: string
deletedAt:
type: string
description:
type: string
foreignPlayers:
type: integer
gender:
type: string
gsmId:
type: integer
id:
type: string
imageDataUrl:
type: string
isActive:
type: boolean
league:
type: string
marketValue:
type: integer
marketValueCurrency:
type: string
name:
type: string
nationalPlayers:
type: integer
officialName:
type: string
season:
type: string
seasonWyId:
type: integer
shortName:
type: string
status:
type: string
totalPlayers:
type: integer
tsId:
type: string
type:
type: string
updatedAt:
type: string
venueTsId:
type: string
website:
type: string
wyId:
type: integer
type: object
info: info:
contact: {} contact: {}
description: API server for scouting system score data. description: API server for scouting system score data.
...@@ -140,8 +470,7 @@ paths: ...@@ -140,8 +470,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
additionalProperties: true $ref: '#/definitions/handlers.CoachListResponse'
type: object
"500": "500":
description: Internal Server Error description: Internal Server Error
schema: schema:
...@@ -164,8 +493,7 @@ paths: ...@@ -164,8 +493,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
additionalProperties: true $ref: '#/definitions/handlers.CoachResponse'
type: object
"404": "404":
description: Not Found description: Not Found
schema: schema:
...@@ -195,8 +523,7 @@ paths: ...@@ -195,8 +523,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
additionalProperties: true $ref: '#/definitions/handlers.CoachResponse'
type: object
"404": "404":
description: Not Found description: Not Found
schema: schema:
...@@ -225,8 +552,7 @@ paths: ...@@ -225,8 +552,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
additionalProperties: true $ref: '#/definitions/handlers.CoachResponse'
type: object
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
...@@ -248,6 +574,100 @@ paths: ...@@ -248,6 +574,100 @@ paths:
summary: Get coach by Wyscout ID summary: Get coach by Wyscout ID
tags: tags:
- Coaches - Coaches
/competitions:
get:
description: Returns a list of competitions with optional pagination.
parameters:
- 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
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: List competitions
tags:
- Competitions
/competitions/{id}:
get:
description: Returns a single competition by its internal ID.
parameters:
- description: Competition internal identifier
in: path
name: id
required: true
type: string
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"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 ID
tags:
- Competitions
/competitions/wyscout/{wyId}:
get:
description: Returns a single competition by its Wyscout wy_id identifier.
parameters:
- description: Wyscout wy_id identifier
in: path
name: wyId
required: true
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"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 Wyscout ID
tags:
- Competitions
/import/areas: /import/areas:
post: post:
description: Imports all countries/regions from TheSports football country list description: Imports all countries/regions from TheSports football country list
...@@ -625,8 +1045,7 @@ paths: ...@@ -625,8 +1045,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
additionalProperties: true $ref: '#/definitions/handlers.MatchListResponse'
type: object
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
...@@ -675,25 +1094,32 @@ paths: ...@@ -675,25 +1094,32 @@ paths:
- Matches - Matches
/matches/head-to-head: /matches/head-to-head:
get: get:
description: Returns the most recent match between teamA and teamB (by ts_id), description: Returns the most recent match where teamHome played at home and
regardless of home/away. teamAway played away (by internal team id).
parameters: parameters:
- description: Team A ts_id - description: Home team internal id
in: query in: query
name: teamA name: teamHome
required: true required: true
type: string type: string
- description: Team B ts_id - description: Away team internal id
in: query in: query
name: teamB name: teamAway
required: true required: true
type: string type: string
- description: Filter matches on/after date (RFC3339 or YYYY-MM-DD)
in: query
name: from
type: string
- description: Filter matches on/before date (RFC3339 or YYYY-MM-DD)
in: query
name: to
type: string
responses: responses:
"200": "200":
description: OK description: OK
schema: schema:
additionalProperties: true $ref: '#/definitions/handlers.MatchResponse'
type: object
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
...@@ -995,9 +1421,9 @@ paths: ...@@ -995,9 +1421,9 @@ paths:
summary: Get referee by Wyscout ID summary: Get referee by Wyscout ID
tags: tags:
- Referees - Referees
/teams: /seasons:
get: get:
description: Returns a list of teams with optional pagination. description: Returns a list of seasons with optional pagination.
parameters: parameters:
- description: Maximum number of items to return (default 100) - description: Maximum number of items to return (default 100)
in: query in: query
...@@ -1020,6 +1446,99 @@ paths: ...@@ -1020,6 +1446,99 @@ paths:
additionalProperties: additionalProperties:
type: string type: string
type: object type: object
summary: List seasons
tags:
- Seasons
/seasons/{id}:
get:
description: Returns a single season by its internal ID.
parameters:
- description: Season internal identifier
in: path
name: id
required: true
type: string
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get season by ID
tags:
- Seasons
/seasons/wyscout/{wyId}:
get:
description: Returns a single season by its Wyscout wy_id identifier.
parameters:
- description: Wyscout wy_id identifier
in: path
name: wyId
required: true
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get season by Wyscout ID
tags:
- Seasons
/teams:
get:
description: Returns a list of teams with optional pagination.
parameters:
- 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
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.TeamListResponse'
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: List teams summary: List teams
tags: tags:
- Teams - Teams
...@@ -1036,8 +1555,7 @@ paths: ...@@ -1036,8 +1555,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
additionalProperties: true $ref: '#/definitions/handlers.TeamResponse'
type: object
"404": "404":
description: Not Found description: Not Found
schema: schema:
...@@ -1066,8 +1584,7 @@ paths: ...@@ -1066,8 +1584,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
additionalProperties: true $ref: '#/definitions/handlers.TeamResponse'
type: object
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
......
...@@ -3,6 +3,7 @@ module ScoutingSystemScoreData ...@@ -3,6 +3,7 @@ module ScoutingSystemScoreData
go 1.25.0 go 1.25.0
require ( require (
github.com/gin-contrib/cors v1.7.0
github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.11.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/matoous/go-nanoid/v2 v2.1.0 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 ...@@ -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/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 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 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 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
......
package handlers package handlers
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
...@@ -14,12 +15,13 @@ import ( ...@@ -14,12 +15,13 @@ import (
) )
type CoachHandler struct { type CoachHandler struct {
DB *gorm.DB
Service services.CoachService Service services.CoachService
} }
func RegisterCoachRoutes(rg *gin.RouterGroup, db *gorm.DB) { func RegisterCoachRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewCoachService(db) service := services.NewCoachService(db)
h := &CoachHandler{Service: service} h := &CoachHandler{DB: db, Service: service}
coaches := rg.Group("/coaches") coaches := rg.Group("/coaches")
coaches.GET("", h.List) coaches.GET("", h.List)
...@@ -32,6 +34,10 @@ type StructuredCoach struct { ...@@ -32,6 +34,10 @@ type StructuredCoach struct {
ID string `json:"id"` ID string `json:"id"`
WyID *int `json:"wyId"` WyID *int `json:"wyId"`
TsID string `json:"tsId"` TsID string `json:"tsId"`
CountryTsID *string `json:"countryTsId"`
CountryName *string `json:"countryName"`
TeamTsID *string `json:"teamTsId"`
TeamName *string `json:"teamName"`
FirstName string `json:"firstName"` FirstName string `json:"firstName"`
LastName string `json:"lastName"` LastName string `json:"lastName"`
MiddleName *string `json:"middleName"` MiddleName *string `json:"middleName"`
...@@ -57,11 +63,102 @@ type StructuredCoach struct { ...@@ -57,11 +63,102 @@ type StructuredCoach struct {
DeletedAt *time.Time `json:"deletedAt"` DeletedAt *time.Time `json:"deletedAt"`
} }
func toStructuredCoach(c models.Coach) StructuredCoach { type CoachListResponse struct {
return StructuredCoach{ 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, ID: c.ID,
WyID: nilIfZero(c.WyID), WyID: nilIfZero(c.WyID),
TsID: c.TsID, TsID: c.TsID,
CountryTsID: c.CountryTsID,
CountryName: countryName,
TeamTsID: c.TeamTsID,
TeamName: teamName,
FirstName: c.FirstName, FirstName: c.FirstName,
LastName: c.LastName, LastName: c.LastName,
MiddleName: c.MiddleName, MiddleName: c.MiddleName,
...@@ -85,7 +182,21 @@ func toStructuredCoach(c models.Coach) StructuredCoach { ...@@ -85,7 +182,21 @@ func toStructuredCoach(c models.Coach) StructuredCoach {
CreatedAt: c.CreatedAt, CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt, UpdatedAt: c.UpdatedAt,
DeletedAt: c.DeletedAt, 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 { func nilIfZero(v *int) *int {
...@@ -108,7 +219,7 @@ 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 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 position query string false "Filter coaches by position (head_coach, assistant_coach, etc.)"
// @Param active query bool false "Filter active coaches only" // @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 // @Failure 500 {object} map[string]string
// @Router /coaches [get] // @Router /coaches [get]
func (h *CoachHandler) List(c *gin.Context) { func (h *CoachHandler) List(c *gin.Context) {
...@@ -154,9 +265,10 @@ func (h *CoachHandler) List(c *gin.Context) { ...@@ -154,9 +265,10 @@ func (h *CoachHandler) List(c *gin.Context) {
return return
} }
structured := make([]StructuredCoach, 0, len(coaches)) structured, err := h.enrichCoaches(c.Request.Context(), coaches)
for _, coach := range coaches { if err != nil {
structured = append(structured, toStructuredCoach(coach)) respondError(c, err)
return
} }
page := offset/limit + 1 page := offset/limit + 1
...@@ -182,7 +294,7 @@ func (h *CoachHandler) List(c *gin.Context) { ...@@ -182,7 +294,7 @@ func (h *CoachHandler) List(c *gin.Context) {
// @Description Returns a single coach by its internal ID. // @Description Returns a single coach by its internal ID.
// @Tags Coaches // @Tags Coaches
// @Param id path string true "Coach internal identifier" // @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 404 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Router /coaches/{id} [get] // @Router /coaches/{id} [get]
...@@ -195,7 +307,11 @@ func (h *CoachHandler) GetByID(c *gin.Context) { ...@@ -195,7 +307,11 @@ func (h *CoachHandler) GetByID(c *gin.Context) {
return 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) timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/coaches/%s", id) endpoint := fmt.Sprintf("/coaches/%s", id)
...@@ -214,7 +330,7 @@ func (h *CoachHandler) GetByID(c *gin.Context) { ...@@ -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. // @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 // @Tags Coaches
// @Param providerId path string true "Provider identifier (wy_id or ts_id)" // @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 404 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Router /coaches/provider/{providerId} [get] // @Router /coaches/provider/{providerId} [get]
...@@ -227,7 +343,11 @@ func (h *CoachHandler) GetByProviderID(c *gin.Context) { ...@@ -227,7 +343,11 @@ func (h *CoachHandler) GetByProviderID(c *gin.Context) {
return 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) timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/coaches/provider/%s", providerID) endpoint := fmt.Sprintf("/coaches/provider/%s", providerID)
...@@ -246,7 +366,7 @@ func (h *CoachHandler) GetByProviderID(c *gin.Context) { ...@@ -246,7 +366,7 @@ func (h *CoachHandler) GetByProviderID(c *gin.Context) {
// @Description Returns a single coach by its Wyscout wy_id identifier. // @Description Returns a single coach by its Wyscout wy_id identifier.
// @Tags Coaches // @Tags Coaches
// @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} handlers.CoachResponse
// @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
...@@ -265,7 +385,11 @@ func (h *CoachHandler) GetByWyID(c *gin.Context) { ...@@ -265,7 +385,11 @@ func (h *CoachHandler) GetByWyID(c *gin.Context) {
return 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) timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/coaches/wyscout/%d", wyID) 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) { ...@@ -1776,7 +1776,13 @@ func (h *ImportHandler) ImportCoaches(c *gin.Context) {
for _, r := range payload.Results { for _, r := range payload.Results {
var coach models.Coach 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 { if err == gorm.ErrRecordNotFound {
coach = models.Coach{ coach = models.Coach{
TsID: r.ID, TsID: r.ID,
...@@ -1841,6 +1847,9 @@ func (h *ImportHandler) ImportCoaches(c *gin.Context) { ...@@ -1841,6 +1847,9 @@ func (h *ImportHandler) ImportCoaches(c *gin.Context) {
} }
// update existing coach with latest data // update existing coach with latest data
if coach.TsID != r.ID {
coach.TsID = r.ID
}
coach.ShortName = strPtrOrNil(r.ShortName) coach.ShortName = strPtrOrNil(r.ShortName)
if r.Birthday != nil { if r.Birthday != nil {
ts := time.Unix(*r.Birthday, 0).UTC() ts := time.Unix(*r.Birthday, 0).UTC()
...@@ -2195,7 +2204,13 @@ func (h *ImportHandler) ImportPlayers(c *gin.Context) { ...@@ -2195,7 +2204,13 @@ func (h *ImportHandler) ImportPlayers(c *gin.Context) {
for _, r := range payload.Results { for _, r := range payload.Results {
var player models.Player 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 { if err == gorm.ErrRecordNotFound {
// new player // new player
player = models.Player{ player = models.Player{
...@@ -2271,6 +2286,9 @@ func (h *ImportHandler) ImportPlayers(c *gin.Context) { ...@@ -2271,6 +2286,9 @@ func (h *ImportHandler) ImportPlayers(c *gin.Context) {
} }
// update existing // update existing
if player.TsID != r.ID {
player.TsID = r.ID
}
player.ShortName = strPtrOrNil(r.ShortName) player.ShortName = strPtrOrNil(r.ShortName)
if r.Birthday != nil { if r.Birthday != nil {
ts := time.Unix(*r.Birthday, 0).UTC() ts := time.Unix(*r.Birthday, 0).UTC()
......
package handlers package handlers
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
...@@ -14,12 +15,13 @@ import ( ...@@ -14,12 +15,13 @@ import (
) )
type MatchHandler struct { type MatchHandler struct {
DB *gorm.DB
Service services.MatchService Service services.MatchService
} }
func RegisterMatchRoutes(rg *gin.RouterGroup, db *gorm.DB) { func RegisterMatchRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewMatchService(db) service := services.NewMatchService(db)
h := &MatchHandler{Service: service} h := &MatchHandler{DB: db, Service: service}
matches := rg.Group("/matches") matches := rg.Group("/matches")
matches.GET("", h.List) matches.GET("", h.List)
...@@ -28,6 +30,215 @@ func RegisterMatchRoutes(rg *gin.RouterGroup, db *gorm.DB) { ...@@ -28,6 +30,215 @@ func RegisterMatchRoutes(rg *gin.RouterGroup, db *gorm.DB) {
matches.GET("/:matchTsId/lineup", h.GetLineupByMatchTsID) matches.GET("/:matchTsId/lineup", h.GetLineupByMatchTsID)
} }
type MatchListResponse struct {
Data []matchOut `json:"data"`
Meta map[string]interface{} `json:"meta"`
}
type MatchResponse struct {
Data matchOut `json:"data"`
Meta map[string]interface{} `json:"meta"`
}
type matchOut struct {
models.Match
HomeTeamID *string `json:"homeTeamId,omitempty"`
HomeTeamName *string `json:"homeTeamName,omitempty"`
AwayTeamID *string `json:"awayTeamId,omitempty"`
AwayTeamName *string `json:"awayTeamName,omitempty"`
CompetitionName *string `json:"competitionName,omitempty"`
SeasonName *string `json:"seasonName,omitempty"`
}
func (h *MatchHandler) enrichMatches(ctx context.Context, matches []models.Match) ([]matchOut, error) {
competitionTsIDs := make([]string, 0, len(matches))
seasonTsIDs := make([]string, 0, len(matches))
matchTsIDsMissingID := make([]string, 0, len(matches))
teamTsIDs := make([]string, 0, len(matches)*2)
seenComp := map[string]struct{}{}
seenSeason := map[string]struct{}{}
seenTeam := map[string]struct{}{}
for _, m := range matches {
if m.ID == "" && m.TsID != "" {
matchTsIDsMissingID = append(matchTsIDsMissingID, m.TsID)
}
if m.HomeTeamTsID != nil && *m.HomeTeamTsID != "" {
if _, ok := seenTeam[*m.HomeTeamTsID]; !ok {
seenTeam[*m.HomeTeamTsID] = struct{}{}
teamTsIDs = append(teamTsIDs, *m.HomeTeamTsID)
}
}
if m.AwayTeamTsID != nil && *m.AwayTeamTsID != "" {
if _, ok := seenTeam[*m.AwayTeamTsID]; !ok {
seenTeam[*m.AwayTeamTsID] = struct{}{}
teamTsIDs = append(teamTsIDs, *m.AwayTeamTsID)
}
}
if m.CompetitionTsID != nil && *m.CompetitionTsID != "" {
if _, ok := seenComp[*m.CompetitionTsID]; !ok {
seenComp[*m.CompetitionTsID] = struct{}{}
competitionTsIDs = append(competitionTsIDs, *m.CompetitionTsID)
}
}
if m.SeasonTsID != nil && *m.SeasonTsID != "" {
if _, ok := seenSeason[*m.SeasonTsID]; !ok {
seenSeason[*m.SeasonTsID] = struct{}{}
seasonTsIDs = append(seasonTsIDs, *m.SeasonTsID)
}
}
}
competitionByTsID := map[string]string{}
if len(competitionTsIDs) > 0 {
var comps []struct {
TsID string `gorm:"column:ts_id"`
Name string `gorm:"column:name"`
}
if err := h.DB.WithContext(ctx).
Model(&models.Competition{}).
Select("ts_id", "name").
Where("ts_id IN ?", competitionTsIDs).
Find(&comps).Error; err != nil {
return nil, err
}
for _, c := range comps {
competitionByTsID[c.TsID] = c.Name
}
}
seasonByTsID := map[string]string{}
if len(seasonTsIDs) > 0 {
var seasons []struct {
TsID string `gorm:"column:ts_id"`
Name string `gorm:"column:name"`
}
if err := h.DB.WithContext(ctx).
Model(&models.Season{}).
Select("ts_id", "name").
Where("ts_id IN ?", seasonTsIDs).
Find(&seasons).Error; err != nil {
return nil, err
}
for _, s := range seasons {
seasonByTsID[s.TsID] = s.Name
}
}
matchIDByTsID := map[string]string{}
if len(matchTsIDsMissingID) > 0 {
var rows []struct {
TsID string `gorm:"column:ts_id"`
ID string `gorm:"column:id"`
}
if err := h.DB.WithContext(ctx).
Model(&models.Match{}).
Select("ts_id", "id").
Where("ts_id IN ?", matchTsIDsMissingID).
Find(&rows).Error; err != nil {
return nil, err
}
for _, r := range rows {
if r.TsID != "" && r.ID != "" {
matchIDByTsID[r.TsID] = r.ID
}
}
}
teamByTsID := map[string]struct {
ID string
Name string
}{}
if len(teamTsIDs) > 0 {
var teams []struct {
TsID string `gorm:"column:ts_id"`
ID string `gorm:"column:id"`
Name string `gorm:"column:name"`
}
if err := h.DB.WithContext(ctx).
Model(&models.Team{}).
Select("ts_id", "id", "name").
Where("ts_id IN ?", teamTsIDs).
Find(&teams).Error; err != nil {
return nil, err
}
for _, t := range teams {
if t.TsID != "" {
teamByTsID[t.TsID] = struct {
ID string
Name string
}{ID: t.ID, Name: t.Name}
}
}
}
out := make([]matchOut, 0, len(matches))
for _, m := range matches {
if m.ID == "" && m.TsID != "" {
if id, ok := matchIDByTsID[m.TsID]; ok {
m.ID = id
}
}
var homeTeamID *string
var homeTeamName *string
if m.HomeTeamTsID != nil {
if t, ok := teamByTsID[*m.HomeTeamTsID]; ok {
id := t.ID
name := t.Name
homeTeamID = &id
homeTeamName = &name
}
}
var awayTeamID *string
var awayTeamName *string
if m.AwayTeamTsID != nil {
if t, ok := teamByTsID[*m.AwayTeamTsID]; ok {
id := t.ID
name := t.Name
awayTeamID = &id
awayTeamName = &name
}
}
var compName *string
if m.CompetitionTsID != nil {
if name, ok := competitionByTsID[*m.CompetitionTsID]; ok {
n := name
compName = &n
}
}
var seasonName *string
if m.SeasonTsID != nil {
if name, ok := seasonByTsID[*m.SeasonTsID]; ok {
n := name
seasonName = &n
}
}
out = append(out, matchOut{
Match: m,
HomeTeamID: homeTeamID,
HomeTeamName: homeTeamName,
AwayTeamID: awayTeamID,
AwayTeamName: awayTeamName,
CompetitionName: compName,
SeasonName: seasonName,
})
}
return out, nil
}
func (h *MatchHandler) enrichMatch(ctx context.Context, match models.Match) (matchOut, error) {
out, err := h.enrichMatches(ctx, []models.Match{match})
if err != nil {
return matchOut{}, err
}
if len(out) == 0 {
return matchOut{Match: match}, nil
}
return out[0], nil
}
// GetLineupByMatchID // GetLineupByMatchID
// @Summary Get match lineup by internal match id // @Summary Get match lineup by internal match id
// @Description Returns the stored lineup data for a match (teams/formations/coaches + players) by internal match id. // @Description Returns the stored lineup data for a match (teams/formations/coaches + players) by internal match id.
...@@ -50,6 +261,12 @@ func (h *MatchHandler) GetLineupByMatchID(c *gin.Context) { ...@@ -50,6 +261,12 @@ func (h *MatchHandler) GetLineupByMatchID(c *gin.Context) {
return return
} }
matchWithNames, err := h.enrichMatch(c.Request.Context(), match)
if err != nil {
respondError(c, err)
return
}
teamTsIDToSide := map[string]string{} teamTsIDToSide := map[string]string{}
for _, t := range teams { for _, t := range teams {
if t.TeamTsID != nil && *t.TeamTsID != "" { if t.TeamTsID != nil && *t.TeamTsID != "" {
...@@ -95,7 +312,7 @@ func (h *MatchHandler) GetLineupByMatchID(c *gin.Context) { ...@@ -95,7 +312,7 @@ func (h *MatchHandler) GetLineupByMatchID(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"data": gin.H{ "data": gin.H{
"match": match, "match": matchWithNames,
"teams": gin.H{ "teams": gin.H{
"home": teamsBySide["home"], "home": teamsBySide["home"],
"away": teamsBySide["away"], "away": teamsBySide["away"],
...@@ -127,7 +344,7 @@ func (h *MatchHandler) GetLineupByMatchID(c *gin.Context) { ...@@ -127,7 +344,7 @@ func (h *MatchHandler) GetLineupByMatchID(c *gin.Context) {
// @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"
// @Param order query string false "Sort order by match_date: asc|desc (default desc)" // @Param order query string false "Sort order by match_date: asc|desc (default desc)"
// @Success 200 {object} map[string]interface{} // @Success 200 {object} handlers.MatchListResponse
// @Failure 400 {object} map[string]string // @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Router /matches [get] // @Router /matches [get]
...@@ -224,6 +441,12 @@ func (h *MatchHandler) List(c *gin.Context) { ...@@ -224,6 +441,12 @@ func (h *MatchHandler) List(c *gin.Context) {
return return
} }
matchesWithNames, err := h.enrichMatches(c.Request.Context(), matches)
if err != nil {
respondError(c, err)
return
}
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)
...@@ -231,7 +454,7 @@ func (h *MatchHandler) List(c *gin.Context) { ...@@ -231,7 +454,7 @@ func (h *MatchHandler) List(c *gin.Context) {
endpoint := "/matches" endpoint := "/matches"
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"data": matches, "data": matchesWithNames,
"meta": gin.H{ "meta": gin.H{
"timestamp": timestamp, "timestamp": timestamp,
"endpoint": endpoint, "endpoint": endpoint,
...@@ -246,34 +469,84 @@ func (h *MatchHandler) List(c *gin.Context) { ...@@ -246,34 +469,84 @@ func (h *MatchHandler) List(c *gin.Context) {
// HeadToHeadMostRecent // HeadToHeadMostRecent
// @Summary Most recent match between two teams // @Summary Most recent match between two teams
// @Description Returns the most recent match between teamA and teamB (by ts_id), regardless of home/away. // @Description Returns the most recent match where teamHome played at home and teamAway played away (by internal team id).
// @Tags Matches // @Tags Matches
// @Param teamA query string true "Team A ts_id" // @Param teamHome query string true "Home team internal id"
// @Param teamB query string true "Team B ts_id" // @Param teamAway query string true "Away team internal id"
// @Success 200 {object} map[string]interface{} // @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
// @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
// @Router /matches/head-to-head [get] // @Router /matches/head-to-head [get]
func (h *MatchHandler) HeadToHeadMostRecent(c *gin.Context) { func (h *MatchHandler) HeadToHeadMostRecent(c *gin.Context) {
teamA := c.Query("teamA") teamHome := c.Query("teamHome")
teamB := c.Query("teamB") teamAway := c.Query("teamAway")
if teamA == "" || teamB == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "teamA and teamB are required"}) parseDate := func(raw string) (*time.Time, error) {
if raw == "" {
return nil, nil
}
if t, err := time.Parse(time.RFC3339, raw); err == nil {
ut := t.UTC()
return &ut, nil
}
if t, err := time.Parse("2006-01-02", raw); err == nil {
ut := t.UTC()
return &ut, nil
}
return nil, fmt.Errorf("invalid date")
}
from, err := parseDate(c.Query("from"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid from (use RFC3339 or YYYY-MM-DD)"})
return
}
to, err := parseDate(c.Query("to"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid to (use RFC3339 or YYYY-MM-DD)"})
return return
} }
match, err := h.Service.GetHeadToHeadMostRecent(c.Request.Context(), teamA, teamB) // Backward compatibility: allow legacy params (teamA/teamB) to map to new behavior.
// NOTE: legacy teamA/teamB previously meant ts_id. Now they are treated as internal team ids.
if teamHome == "" {
teamHome = c.Query("teamA")
}
if teamAway == "" {
teamAway = c.Query("teamB")
}
if teamHome == "" || teamAway == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "teamHome and teamAway are required"})
return
}
match, err := h.Service.GetHeadToHeadMostRecent(c.Request.Context(), teamHome, teamAway, from, to)
if err != nil {
respondError(c, err)
return
}
matchWithNames, err := h.enrichMatch(c.Request.Context(), match)
if err != nil { if err != nil {
respondError(c, err) respondError(c, err)
return return
} }
timestamp := time.Now().UTC().Format(time.RFC3339) timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/matches/head-to-head?teamA=%s&teamB=%s", teamA, teamB) endpoint := fmt.Sprintf("/matches/head-to-head?teamHome=%s&teamAway=%s", teamHome, teamAway)
if from != nil {
endpoint = fmt.Sprintf("%s&from=%s", endpoint, from.UTC().Format(time.RFC3339))
}
if to != nil {
endpoint = fmt.Sprintf("%s&to=%s", endpoint, to.UTC().Format(time.RFC3339))
}
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"data": match, "data": matchWithNames,
"meta": gin.H{ "meta": gin.H{
"timestamp": timestamp, "timestamp": timestamp,
"endpoint": endpoint, "endpoint": endpoint,
...@@ -304,6 +577,12 @@ func (h *MatchHandler) GetLineupByMatchTsID(c *gin.Context) { ...@@ -304,6 +577,12 @@ func (h *MatchHandler) GetLineupByMatchTsID(c *gin.Context) {
return return
} }
matchWithNames, err := h.enrichMatch(c.Request.Context(), match)
if err != nil {
respondError(c, err)
return
}
teamTsIDToSide := map[string]string{} teamTsIDToSide := map[string]string{}
for _, t := range teams { for _, t := range teams {
if t.TeamTsID != nil && *t.TeamTsID != "" { if t.TeamTsID != nil && *t.TeamTsID != "" {
...@@ -351,7 +630,7 @@ func (h *MatchHandler) GetLineupByMatchTsID(c *gin.Context) { ...@@ -351,7 +630,7 @@ func (h *MatchHandler) GetLineupByMatchTsID(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"data": gin.H{ "data": gin.H{
"match": match, "match": matchWithNames,
"teams": gin.H{ "teams": gin.H{
"home": teamsBySide["home"], "home": teamsBySide["home"],
"away": teamsBySide["away"], "away": teamsBySide["away"],
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
...@@ -15,11 +16,15 @@ import ( ...@@ -15,11 +16,15 @@ import (
type PlayerHandler struct { type PlayerHandler struct {
Service services.PlayerService Service services.PlayerService
Teams services.TeamService
Areas services.AreaService
} }
func RegisterPlayerRoutes(rg *gin.RouterGroup, db *gorm.DB) { func RegisterPlayerRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewPlayerService(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 := rg.Group("/players")
players.GET("", h.List) players.GET("", h.List)
...@@ -47,6 +52,7 @@ type Meta struct { ...@@ -47,6 +52,7 @@ type Meta struct {
type StructuredPlayer struct { type StructuredPlayer struct {
ID string `json:"id"` ID string `json:"id"`
TsID string `json:"tsId"`
WyID *int `json:"wyId"` WyID *int `json:"wyId"`
GSMID *int `json:"gsmId"` GSMID *int `json:"gsmId"`
ShortName string `json:"shortName"` ShortName string `json:"shortName"`
...@@ -61,6 +67,8 @@ type StructuredPlayer struct { ...@@ -61,6 +67,8 @@ type StructuredPlayer struct {
Foot *string `json:"foot"` Foot *string `json:"foot"`
CurrentTeamID *int `json:"currentTeamId"` CurrentTeamID *int `json:"currentTeamId"`
CurrentNationalTeamID *int `json:"currentNationalTeamId"` CurrentNationalTeamID *int `json:"currentNationalTeamId"`
CountryTsID *string `json:"countryTsId"`
CountryName *string `json:"countryName"`
Gender *string `json:"gender"` Gender *string `json:"gender"`
Status string `json:"status"` Status string `json:"status"`
JerseyNumber *int `json:"jerseyNumber"` JerseyNumber *int `json:"jerseyNumber"`
...@@ -78,6 +86,16 @@ type StructuredPlayer struct { ...@@ -78,6 +86,16 @@ type StructuredPlayer struct {
UID *string `json:"uid"` UID *string `json:"uid"`
Deathday *time.Time `json:"deathday"` Deathday *time.Time `json:"deathday"`
RetireTime *time.Time `json:"retireTime"` 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 { func toStructuredPlayer(p models.Player) StructuredPlayer {
...@@ -107,6 +125,7 @@ func toStructuredPlayer(p models.Player) StructuredPlayer { ...@@ -107,6 +125,7 @@ func toStructuredPlayer(p models.Player) StructuredPlayer {
return StructuredPlayer{ return StructuredPlayer{
ID: p.ID, ID: p.ID,
TsID: p.TsID,
WyID: p.WyID, WyID: p.WyID,
GSMID: p.GSMID, GSMID: p.GSMID,
ShortName: valueOrDefault(p.ShortName, ""), ShortName: valueOrDefault(p.ShortName, ""),
...@@ -121,6 +140,8 @@ func toStructuredPlayer(p models.Player) StructuredPlayer { ...@@ -121,6 +140,8 @@ func toStructuredPlayer(p models.Player) StructuredPlayer {
Foot: p.Foot, Foot: p.Foot,
CurrentTeamID: p.CurrentTeamID, CurrentTeamID: p.CurrentTeamID,
CurrentNationalTeamID: p.CurrentNationalTeamID, CurrentNationalTeamID: p.CurrentNationalTeamID,
CountryTsID: p.CountryTsID,
CountryName: nil,
Gender: p.Gender, Gender: p.Gender,
Status: status, Status: status,
JerseyNumber: p.JerseyNumber, JerseyNumber: p.JerseyNumber,
...@@ -140,6 +161,51 @@ func toStructuredPlayer(p models.Player) StructuredPlayer { ...@@ -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 { func valueOrDefault(s *string, def string) string {
if s != nil { if s != nil {
return *s return *s
...@@ -201,6 +267,64 @@ func (h *PlayerHandler) List(c *gin.Context) { ...@@ -201,6 +267,64 @@ func (h *PlayerHandler) List(c *gin.Context) {
structured = append(structured, toStructuredPlayer(p)) 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 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)
...@@ -237,6 +361,34 @@ func (h *PlayerHandler) GetByID(c *gin.Context) { ...@@ -237,6 +361,34 @@ func (h *PlayerHandler) GetByID(c *gin.Context) {
} }
structured := toStructuredPlayer(player) 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) timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/players/%s", id) endpoint := fmt.Sprintf("/players/%s", id)
...@@ -261,6 +413,35 @@ func (h *PlayerHandler) GetByAnyProviderID(c *gin.Context) { ...@@ -261,6 +413,35 @@ func (h *PlayerHandler) GetByAnyProviderID(c *gin.Context) {
} }
structured := toStructuredPlayer(player) 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) timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/players/provider/%s", providerID) endpoint := fmt.Sprintf("/players/provider/%s", providerID)
...@@ -298,6 +479,34 @@ func (h *PlayerHandler) GetByProviderID(c *gin.Context) { ...@@ -298,6 +479,34 @@ func (h *PlayerHandler) GetByProviderID(c *gin.Context) {
} }
structured := toStructuredPlayer(player) 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) timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/players/wyscout/%d", wyID) 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 ( ...@@ -9,9 +9,20 @@ 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 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 { type TeamHandler struct {
Service services.TeamService Service services.TeamService
} }
...@@ -33,7 +44,7 @@ func RegisterTeamRoutes(rg *gin.RouterGroup, db *gorm.DB) { ...@@ -33,7 +44,7 @@ func RegisterTeamRoutes(rg *gin.RouterGroup, db *gorm.DB) {
// @Tags Teams // @Tags Teams
// @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{} // @Success 200 {object} handlers.TeamListResponse
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Router /teams [get] // @Router /teams [get]
func (h *TeamHandler) List(c *gin.Context) { func (h *TeamHandler) List(c *gin.Context) {
...@@ -49,7 +60,14 @@ func (h *TeamHandler) List(c *gin.Context) { ...@@ -49,7 +60,14 @@ func (h *TeamHandler) List(c *gin.Context) {
offset = 0 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 { if err != nil {
respondError(c, err) respondError(c, err)
return return
...@@ -63,7 +81,7 @@ func (h *TeamHandler) List(c *gin.Context) { ...@@ -63,7 +81,7 @@ func (h *TeamHandler) List(c *gin.Context) {
"data": teams, "data": teams,
"meta": gin.H{ "meta": gin.H{
"timestamp": timestamp, "timestamp": timestamp,
"endpoint": "/teams", "endpoint": endpoint,
"method": "GET", "method": "GET",
"totalItems": total, "totalItems": total,
"page": page, "page": page,
...@@ -78,7 +96,7 @@ func (h *TeamHandler) List(c *gin.Context) { ...@@ -78,7 +96,7 @@ func (h *TeamHandler) List(c *gin.Context) {
// @Description Returns a single team by its internal ID. // @Description Returns a single team by its internal ID.
// @Tags Teams // @Tags Teams
// @Param id path string true "Team internal identifier" // @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 404 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Router /teams/{id} [get] // @Router /teams/{id} [get]
...@@ -132,7 +150,7 @@ func (h *TeamHandler) GetByProviderID(c *gin.Context) { ...@@ -132,7 +150,7 @@ func (h *TeamHandler) GetByProviderID(c *gin.Context) {
// @Description Returns a single team by its Wyscout wy_id identifier. // @Description Returns a single team by its Wyscout wy_id identifier.
// @Tags Teams // @Tags Teams
// @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} handlers.TeamResponse
// @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
......
package models package models
import ( import (
"encoding/json"
"time" "time"
gonanoid "github.com/matoous/go-nanoid/v2" gonanoid "github.com/matoous/go-nanoid/v2"
...@@ -109,13 +110,23 @@ type Team struct { ...@@ -109,13 +110,23 @@ type Team struct {
Type string `gorm:"column:type;type:team_type;default:club" json:"type"` Type string `gorm:"column:type;type:team_type;default:club" json:"type"`
Category string `gorm:"column:category;default:default" json:"category"` Category string `gorm:"column:category;default:default" json:"category"`
Gender *string `gorm:"column:gender;type:gender" json:"gender"` 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"` AreaWyID *int `gorm:"column:area_wy_id" json:"areaWyId"`
City *string `gorm:"column:city" json:"city"` City *string `gorm:"column:city" json:"city"`
CoachWyID *int `gorm:"column:coach_wy_id" json:"coachWyId"` 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"` CompetitionWyID *int `gorm:"column:competition_wy_id" json:"competitionWyId"`
SeasonWyID *int `gorm:"column:season_wy_id" json:"seasonWyId"` SeasonWyID *int `gorm:"column:season_wy_id" json:"seasonWyId"`
League *string `gorm:"column:league" json:"league"` League *string `gorm:"column:league" json:"league"`
Season *string `gorm:"column:season" json:"season"` 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"` ImageDataURL *string `gorm:"column:image_data_url" json:"imageDataUrl"`
Status string `gorm:"column:status;type:status;default:active" json:"status"` Status string `gorm:"column:status;type:status;default:active" json:"status"`
IsActive bool `gorm:"column:is_active;default:true" json:"isActive"` IsActive bool `gorm:"column:is_active;default:true" json:"isActive"`
...@@ -158,6 +169,8 @@ type Coach struct { ...@@ -158,6 +169,8 @@ type Coach struct {
ID string `gorm:"primaryKey;size:16" json:"id"` ID string `gorm:"primaryKey;size:16" json:"id"`
WyID *int `gorm:"column:wy_id;uniqueIndex" json:"wyId"` WyID *int `gorm:"column:wy_id;uniqueIndex" json:"wyId"`
TsID string `gorm:"column:ts_id;size:64" json:"tsId"` 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"` FirstName string `gorm:"column:first_name" json:"firstName"`
LastName string `gorm:"column:last_name" json:"lastName"` LastName string `gorm:"column:last_name" json:"lastName"`
MiddleName *string `gorm:"column:middle_name" json:"middleName"` MiddleName *string `gorm:"column:middle_name" json:"middleName"`
...@@ -227,6 +240,7 @@ type Player struct { ...@@ -227,6 +240,7 @@ type Player struct {
TsID string `gorm:"column:ts_id;size:64" json:"tsId"` TsID string `gorm:"column:ts_id;size:64" json:"tsId"`
TeamWyID *int `gorm:"column:team_wy_id" json:"teamWyId"` TeamWyID *int `gorm:"column:team_wy_id" json:"teamWyId"`
TeamTsID *string `gorm:"column:team_ts_id;size:64" json:"teamTsId"` 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"` FirstName string `gorm:"column:first_name" json:"firstName"`
LastName string `gorm:"column:last_name" json:"lastName"` LastName string `gorm:"column:last_name" json:"lastName"`
MiddleName *string `gorm:"column:middle_name" json:"middleName"` MiddleName *string `gorm:"column:middle_name" json:"middleName"`
...@@ -282,6 +296,8 @@ type Match struct { ...@@ -282,6 +296,8 @@ type Match struct {
ID string `gorm:"primaryKey;size:16" json:"id"` ID string `gorm:"primaryKey;size:16" json:"id"`
WyID *int `gorm:"column:wy_id;uniqueIndex" json:"wyId"` WyID *int `gorm:"column:wy_id;uniqueIndex" json:"wyId"`
TsID string `gorm:"column:ts_id;size:64;uniqueIndex" json:"tsId"` 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"` HomeTeamWyID *int `gorm:"column:home_team_wy_id" json:"homeTeamWyId"`
HomeTeamTsID *string `gorm:"column:home_team_ts_id;size:64" json:"homeTeamTsId"` HomeTeamTsID *string `gorm:"column:home_team_ts_id;size:64" json:"homeTeamTsId"`
AwayTeamWyID *int `gorm:"column:away_team_wy_id" json:"awayTeamWyId"` AwayTeamWyID *int `gorm:"column:away_team_wy_id" json:"awayTeamWyId"`
...@@ -290,6 +306,11 @@ type Match struct { ...@@ -290,6 +306,11 @@ type Match struct {
SeasonWyID *int `gorm:"column:season_wy_id" json:"seasonWyId"` SeasonWyID *int `gorm:"column:season_wy_id" json:"seasonWyId"`
RoundWyID *int `gorm:"column:round_wy_id" json:"roundWyId"` RoundWyID *int `gorm:"column:round_wy_id" json:"roundWyId"`
MatchDate time.Time `gorm:"column:match_date" json:"matchDate"` 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"` Venue *string `gorm:"column:venue" json:"venue"`
VenueCity *string `gorm:"column:venue_city" json:"venueCity"` VenueCity *string `gorm:"column:venue_city" json:"venueCity"`
VenueCountry *string `gorm:"column:venue_country" json:"venueCountry"` VenueCountry *string `gorm:"column:venue_country" json:"venueCountry"`
...@@ -299,6 +320,17 @@ type Match struct { ...@@ -299,6 +320,17 @@ type Match struct {
AwayScore int `gorm:"column:away_score;default:0" json:"awayScore"` AwayScore int `gorm:"column:away_score;default:0" json:"awayScore"`
HomeScorePenalties int `gorm:"column:home_score_penalties;default:0" json:"homeScorePenalties"` HomeScorePenalties int `gorm:"column:home_score_penalties;default:0" json:"homeScorePenalties"`
AwayScorePenalties int `gorm:"column:away_score_penalties;default:0" json:"awayScorePenalties"` 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"` Attendance *int `gorm:"column:attendance" json:"attendance"`
MainRefereeWyID *int `gorm:"column:main_referee_wy_id" json:"mainRefereeWyId"` MainRefereeWyID *int `gorm:"column:main_referee_wy_id" json:"mainRefereeWyId"`
AssistantReferee1WyID *int `gorm:"column:assistant_referee_1_wy_id" json:"assistantReferee1WyId"` AssistantReferee1WyID *int `gorm:"column:assistant_referee_1_wy_id" json:"assistantReferee1WyId"`
...@@ -307,6 +339,15 @@ type Match struct { ...@@ -307,6 +339,15 @@ type Match struct {
VarRefereeWyID *int `gorm:"column:var_referee_wy_id" json:"varRefereeWyId"` VarRefereeWyID *int `gorm:"column:var_referee_wy_id" json:"varRefereeWyId"`
Weather *string `gorm:"column:weather" json:"weather"` Weather *string `gorm:"column:weather" json:"weather"`
Temperature *float64 `gorm:"column:temperature" json:"temperature"` 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"` 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"` APISyncStatus string `gorm:"column:api_sync_status;type:api_sync_status;default:pending" json:"apiSyncStatus"`
Notes *string `gorm:"column:notes" json:"notes"` Notes *string `gorm:"column:notes" json:"notes"`
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
...@@ -84,6 +85,21 @@ func basicAuthMiddleware() gin.HandlerFunc { ...@@ -84,6 +85,21 @@ func basicAuthMiddleware() gin.HandlerFunc {
func New(db *gorm.DB) *gin.Engine { func New(db *gorm.DB) *gin.Engine {
r := gin.Default() 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) { r.GET("/health", func(c *gin.Context) {
if sqlDB, err := db.DB(); err == nil { if sqlDB, err := db.DB(); err == nil {
if err := sqlDB.Ping(); err != nil { if err := sqlDB.Ping(); err != nil {
...@@ -106,6 +122,8 @@ func New(db *gorm.DB) *gin.Engine { ...@@ -106,6 +122,8 @@ func New(db *gorm.DB) *gin.Engine {
api.Use(basicAuthMiddleware()) api.Use(basicAuthMiddleware())
appCfg := config.Load() appCfg := config.Load()
handlers.RegisterAreaRoutes(api, db) handlers.RegisterAreaRoutes(api, db)
handlers.RegisterCompetitionRoutes(api, db)
handlers.RegisterSeasonRoutes(api, db)
handlers.RegisterTeamRoutes(api, db) handlers.RegisterTeamRoutes(api, db)
handlers.RegisterPlayerRoutes(api, db) handlers.RegisterPlayerRoutes(api, db)
handlers.RegisterCoachRoutes(api, db) handlers.RegisterCoachRoutes(api, db)
......
...@@ -22,6 +22,7 @@ type AreaService interface { ...@@ -22,6 +22,7 @@ type AreaService interface {
ListAreas(ctx context.Context, opts ListAreasOptions) ([]models.Area, int64, error) ListAreas(ctx context.Context, opts ListAreasOptions) ([]models.Area, int64, error)
GetByID(ctx context.Context, id string) (models.Area, error) GetByID(ctx context.Context, id string) (models.Area, error)
GetByProviderID(ctx context.Context, providerID 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 { type areaService struct {
...@@ -88,3 +89,16 @@ func (s *areaService) GetByProviderID(ctx context.Context, providerID string) (m ...@@ -88,3 +89,16 @@ func (s *areaService) GetByProviderID(ctx context.Context, providerID string) (m
} }
return area, nil 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 ...@@ -2,6 +2,7 @@ package services
import ( import (
"context" "context"
"fmt"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
...@@ -14,7 +15,7 @@ type MatchService interface { ...@@ -14,7 +15,7 @@ type MatchService interface {
ListMatches(ctx context.Context, opts ListMatchesOptions) ([]models.Match, int64, error) ListMatches(ctx context.Context, opts ListMatchesOptions) ([]models.Match, int64, error)
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, 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) 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)
} }
...@@ -102,15 +103,43 @@ func (s *matchService) GetByID(ctx context.Context, id string) (models.Match, er ...@@ -102,15 +103,43 @@ func (s *matchService) GetByID(ctx context.Context, id string) (models.Match, er
return match, nil 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 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{}). 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") 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 := q.First(&match).Error; err != nil {
if err == gorm.ErrRecordNotFound { 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) return models.Match{}, errors.Wrap(errors.CodeInternal, "failed to fetch head-to-head match", err)
} }
......
...@@ -81,11 +81,17 @@ func (s *playerService) GetByAnyProviderID(ctx context.Context, providerID strin ...@@ -81,11 +81,17 @@ func (s *playerService) GetByAnyProviderID(ctx context.Context, providerID strin
var player models.Player var player models.Player
if err := s.db.WithContext(ctx).First(&player, "ts_id = ?", providerID).Error; err != nil { if err := s.db.WithContext(ctx).First(&player, "ts_id = ?", providerID).Error; err != nil {
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
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.New(errors.CodeNotFound, "player not found")
} }
return models.Player{}, errors.Wrap(errors.CodeInternal, "failed to fetch player", err) return models.Player{}, errors.Wrap(errors.CodeInternal, "failed to fetch player", err)
} }
return player, nil return player, nil
}
return models.Player{}, errors.Wrap(errors.CodeInternal, "failed to fetch player", err)
}
return player, nil
} }
func (s *playerService) GetByProviderID(ctx context.Context, wyID int) (models.Player, error) { func (s *playerService) GetByProviderID(ctx context.Context, wyID int) (models.Player, error) {
......
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 ( ...@@ -11,10 +11,11 @@ import (
) )
type TeamService interface { 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) GetByID(ctx context.Context, id string) (models.Team, error)
GetByWyID(ctx context.Context, wyID int) (models.Team, error) GetByWyID(ctx context.Context, wyID int) (models.Team, error)
GetByAnyProviderID(ctx context.Context, providerID string) (models.Team, error) GetByAnyProviderID(ctx context.Context, providerID string) (models.Team, error)
GetByTsIDs(ctx context.Context, tsIDs []string) ([]models.Team, error)
} }
type teamService struct { type teamService struct {
...@@ -25,9 +26,12 @@ func NewTeamService(db *gorm.DB) TeamService { ...@@ -25,9 +26,12 @@ func NewTeamService(db *gorm.DB) TeamService {
return &teamService{db: db} 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 var teams []models.Team
query := s.db.WithContext(ctx).Model(&models.Team{}) query := s.db.WithContext(ctx).Model(&models.Team{})
if name != "" {
query = query.Where("name ILIKE ?", "%"+name+"%")
}
var total int64 var total int64
if err := query.Count(&total).Error; err != nil { if err := query.Count(&total).Error; err != nil {
...@@ -78,3 +82,16 @@ func (s *teamService) GetByWyID(ctx context.Context, wyID int) (models.Team, err ...@@ -78,3 +82,16 @@ func (s *teamService) GetByWyID(ctx context.Context, wyID int) (models.Team, err
} }
return team, nil 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