Commit 90a1bdcf by Augusto

new data and endpoint fix

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