Commit 297c6c7e by Augusto

first WS version

parents
root = "."
[build]
cmd = "go build -o ./bin/server ./cmd/server"
entrypoint = "./bin/server"
include_ext = ["go"]
exclude_dir = ["vendor", "tmp"]
[log]
time = true
[color]
main = "magenta"
.env
.env.*
# Air
/tmp
.air.toml.tmp
# Go build artifacts
/bin/
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
# Go workspace
/go.work
/go.work.sum
# IDE
.vscode/
.idea/
*.swp
.DS_Store
# Logs
*.log
# Docker
**/.docker/
{
"info": {
"_postman_id": "4551fb71-0e70-4208-a15b-852c7d755a6e",
"name": "API Sample Calls Copy",
"description": "Samples for trial purposes",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "25021916"
},
"item": [
{
"name": "Search Query on Players",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/search?query=dybala&objType=player",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"search"
],
"query": [
{
"key": "query",
"value": "dybala"
},
{
"key": "objType",
"value": "player"
}
]
}
},
"response": []
},
{
"name": "Search Query on Teams",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/search?query=juventus&objType=team",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"search"
],
"query": [
{
"key": "query",
"value": "juventus"
},
{
"key": "objType",
"value": "team"
}
]
}
},
"response": []
},
{
"name": "Search Query on Referees",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/search?query=pairetto&objType=referee&imageDataURL=true ",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"search"
],
"query": [
{
"key": "query",
"value": "pairetto"
},
{
"key": "objType",
"value": "referee"
},
{
"key": "imageDataURL",
"value": "true "
}
]
}
},
"response": []
},
{
"name": "Search Query on Competitions",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/search?query=serie a&objType=competition",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"search"
],
"query": [
{
"key": "query",
"value": "serie a"
},
{
"key": "objType",
"value": "competition"
}
]
}
},
"response": []
},
{
"name": "Search Query on Players (women)",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/search?query=gama&objType=player&gender=women",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"search"
],
"query": [
{
"key": "query",
"value": "gama"
},
{
"key": "objType",
"value": "player"
},
{
"key": "gender",
"value": "women"
}
]
}
},
"response": []
},
{
"name": "Profile Coaches WyID",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/coaches/20386",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"coaches",
"20386"
]
}
},
"response": []
},
{
"name": "Areas",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/areas",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"areas"
]
}
},
"response": []
},
{
"name": "Competitions",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/competitions?areaId=ita",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"competitions"
],
"query": [
{
"key": "areaId",
"value": "ita"
}
]
}
},
"response": []
},
{
"name": "Competitions WyID",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/competitions/524",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"competitions",
"524"
]
}
},
"response": []
},
{
"name": "Competitions WyID Seasons",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/competitions/524/seasons",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"competitions",
"524",
"seasons"
]
}
},
"response": []
},
{
"name": "Competitions WyID Matches",
"event": [
{
"listen": "prerequest",
"script": {
"exec": [
""
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/competitions/524/matches",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"competitions",
"524",
"matches"
]
}
},
"response": []
},
{
"name": "Competitions WyID Players",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/competitions/524/players?limit=100&page=1",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"competitions",
"524",
"players"
],
"query": [
{
"key": "limit",
"value": "100"
},
{
"key": "page",
"value": "1"
}
]
}
},
"response": []
},
{
"name": "Competitions WyID Teams",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value":"",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v2/competitions/524/teams",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v2",
"competitions",
"524",
"teams"
]
}
},
"response": []
},
{
"name": "Matches WyID",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/matches/5476266?useSides=1",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"matches",
"5476266"
],
"query": [
{
"key": "useSides",
"value": "1"
}
]
}
},
"response": []
},
{
"name": "Matches WyID Formations",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirestrc.wyscout.com/v4/matches/5476266/formations",
"protocol": "https",
"host": [
"apirestrc",
"wyscout",
"com"
],
"path": [
"v4",
"matches",
"5476266",
"formations"
]
}
},
"response": []
},
{
"name": "Players WyID",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/players/89186/",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"players",
"89186",
""
]
}
},
"response": []
},
{
"name": "Players WyID Career",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/players/89186/career",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"players",
"89186",
"career"
]
}
},
"response": []
},
{
"name": "Players WyID Transfers",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/players/89186/transfers",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"players",
"89186",
"transfers"
]
}
},
"response": []
},
{
"name": "Teams WyID",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/teams/3158",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"teams",
"3158"
]
}
},
"response": []
},
{
"name": "Players WyID Matches",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/players/89186/matches",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"players",
"89186",
"matches"
]
}
},
"response": []
},
{
"name": "Players WyID Fixtures",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/players/89186/fixtures",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"players",
"89186",
"fixtures"
]
}
},
"response": []
},
{
"name": "Referees WyID",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/referees/377257",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"referees",
"377257"
]
}
},
"response": []
},
{
"name": "Rounds WyID",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/rounds/4429534",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"rounds",
"4429534"
]
}
},
"response": []
},
{
"name": "Seasons WyID Matches",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/seasons/188994/matches",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"seasons",
"188994",
"matches"
]
}
},
"response": []
},
{
"name": "Seasons WyID",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/seasons/188994?details=competition",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"seasons",
"188994"
],
"query": [
{
"key": "details",
"value": "competition"
}
]
}
},
"response": []
},
{
"name": "Seasons WyID Career",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/seasons/188994/career?details=round",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"seasons",
"188994",
"career"
],
"query": [
{
"key": "details",
"value": "round"
}
]
}
},
"response": []
},
{
"name": "Seasons WyID Players",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/seasons/188994/players?limit=100&page=1",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"seasons",
"188994",
"players"
],
"query": [
{
"key": "limit",
"value": "100"
},
{
"key": "page",
"value": "1"
}
]
}
},
"response": []
},
{
"name": "Seasons WyID Teams",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/seasons/188994/teams",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"seasons",
"188994",
"teams"
]
}
},
"response": []
},
{
"name": "Seasons WyID Fixtures",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/seasons/188994/fixtures?details=matches",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"seasons",
"188994",
"fixtures"
],
"query": [
{
"key": "details",
"value": "matches"
}
]
}
},
"response": []
},
{
"name": "Teams WyID Career",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/teams/3158/career?details=competition,season",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"teams",
"3158",
"career"
],
"query": [
{
"key": "details",
"value": "competition,season"
}
]
}
},
"response": []
},
{
"name": "Seasons WyID Standings",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/seasons/188994/standings",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"seasons",
"188994",
"standings"
]
}
},
"response": []
},
{
"name": "Teams WyID Matches",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/teams/3158/matches",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"teams",
"3158",
"matches"
]
}
},
"response": []
},
{
"name": "Teams WyID Squad",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/teams/3158/squad",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"teams",
"3158",
"squad"
]
}
},
"response": []
},
{
"name": "Teams WyID Fixtures",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/teams/3158/fixtures",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"teams",
"3158",
"fixtures"
]
}
},
"response": []
},
{
"name": "Players WyID Advanced Stats",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/players/89186/advancedstats?compId=524&seasonId=188994",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"players",
"89186",
"advancedstats"
],
"query": [
{
"key": "compId",
"value": "524"
},
{
"key": "seasonId",
"value": "188994"
}
]
}
},
"response": []
},
{
"name": "Teams WyID Advanced Stats",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/teams/3158/advancedstats?compId=524&seasonId=188994",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"teams",
"3158",
"advancedstats"
],
"query": [
{
"key": "compId",
"value": "524"
},
{
"key": "seasonId",
"value": "188994"
}
]
}
},
"response": []
},
{
"name": "Matches WyID Advanced Stats",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/matches/5476266/advancedstats?useSides=1",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"matches",
"5476266",
"advancedstats"
],
"query": [
{
"key": "useSides",
"value": "1"
}
]
}
},
"response": []
},
{
"name": "Players WyID Matches WyID Advanced Stats",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/players/89186/matches/5476266/advancedstats?details=player",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"players",
"89186",
"matches",
"5476266",
"advancedstats"
],
"query": [
{
"key": "details",
"value": "player"
}
]
}
},
"response": []
},
{
"name": "Teams WyID Matches WyID Advanced Stats",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/teams/3158/matches/5476266/advancedstats",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"teams",
"3158",
"matches",
"5476266",
"advancedstats"
]
}
},
"response": []
},
{
"name": "V3 Matches WyID events",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "username",
"value": "",
"type": "string"
},
{
"key": "password",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/matches/5476266/events",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"matches",
"5476266",
"events"
]
}
},
"response": []
},
{
"name": "Videos",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/videos/5476262",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"videos",
"5476262"
]
}
},
"response": []
},
{
"name": "Videos Offsets",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/videos/5476262/offsets",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"videos",
"5476262",
"offsets"
]
}
},
"response": []
},
{
"name": "Videos Qualities",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/videos/5476262/qualities",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"videos",
"5476262",
"qualities"
]
}
},
"response": []
},
{
"name": "Videos Clip",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/videos/5476262?start=100&end=110",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"videos",
"5476262"
],
"query": [
{
"key": "start",
"value": "100"
},
{
"key": "end",
"value": "110"
}
]
}
},
"response": []
},
{
"name": "Updated Objects Updated",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v4/updatedobjects/updated?updated_since=2023-09-10 00:00:00&type=matchevents&limit=100&page=1",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v4",
"updatedobjects",
"updated"
],
"query": [
{
"key": "updated_since",
"value": "2023-09-10 00:00:00"
},
{
"key": "type",
"value": "matchevents"
},
{
"key": "limit",
"value": "100"
},
{
"key": "page",
"value": "1"
}
]
}
},
"response": []
},
{
"name": "Updated Objects Deleted",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v4/updatedobjects/deleted?updated_since=2023-09-10 00:00:00&type=matchevents&limit=100&page=1",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v4",
"updatedobjects",
"deleted"
],
"query": [
{
"key": "updated_since",
"value": "2023-09-10 00:00:00"
},
{
"key": "type",
"value": "matchevents"
},
{
"key": "limit",
"value": "100"
},
{
"key": "page",
"value": "1"
}
]
}
},
"response": []
},
{
"name": "All Players in Match Advancedstats V3",
"request": {
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://apirest.wyscout.com/v3/matches/5476266/advancedstats/players",
"protocol": "https",
"host": [
"apirest",
"wyscout",
"com"
],
"path": [
"v3",
"matches",
"5476266",
"advancedstats",
"players"
]
}
},
"response": []
}
]
}
\ No newline at end of file
FROM golang:1.25-alpine AS builder
WORKDIR /src
RUN apk add --no-cache ca-certificates tzdata
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/server ./cmd/server
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
COPY --from=builder /out/server /app/server
EXPOSE 3001
CMD ["/app/server"]
-- SQL to clear lastName fields for referees and coaches
-- where the entire name was duplicated in both firstName and lastName
-- Clear lastName for referees where firstName = lastName
UPDATE referees
SET last_name = NULL
WHERE first_name = last_name;
-- Clear lastName for coaches where firstName = lastName
UPDATE coaches
SET last_name = NULL
WHERE first_name = last_name;
-- Optional: Verify the changes before running
-- (Uncomment to check what will be updated)
-- Show referees that will be updated
-- SELECT id, first_name, last_name
-- FROM referees
-- WHERE first_name = last_name;
-- Show coaches that will be updated
-- SELECT id, first_name, last_name
-- FROM coaches
-- WHERE first_name = last_name;
-- After running the updates, verify the changes
-- SELECT COUNT(*) as referees_updated
-- FROM referees
-- WHERE last_name IS NULL AND first_name IS NOT NULL;
-- SELECT COUNT(*) as coaches_updated
-- FROM coaches
-- WHERE last_name IS NULL AND first_name IS NOT NULL;
package main
import (
"log"
"os"
"github.com/joho/godotenv"
"ScoutingSystemScoreData/internal/config"
"ScoutingSystemScoreData/internal/database"
"ScoutingSystemScoreData/internal/router"
)
// @title Scouting System Score Data API
// @version 1.0
// @description API server for scouting system score data.
// @BasePath /api
func main() {
if err := godotenv.Load(".env"); err != nil {
log.Printf("warning: could not load .env file: %v", err)
}
cfg := config.Load()
db, err := database.Connect(cfg)
if err != nil {
log.Fatalf("failed to connect to database: %v", err)
}
r := router.New(db)
port := cfg.Port
if port == "" {
port = os.Getenv("PORT")
}
if port == "" {
port = "3003"
}
if err := r.Run(":" + port); err != nil {
log.Fatalf("failed to run server: %v", err)
}
}
services:
SSEData:
container_name: SSEData
build:
context: .
dockerfile: Dockerfile
env_file:
- ./.env
ports:
- "3001:3001"
restart: unless-stopped
postgres:
image: postgres:17-alpine
container_name: SSEData-Postgres
profiles:
- with-db
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
ports:
- "5433:5432"
volumes:
- ssedata_pgdata:/var/lib/postgresql/data
restart: unless-stopped
volumes:
ssedata_pgdata:
# Docker Setup (SSSData)
This project can be run locally and deployed to a VPS using Docker Compose.
## Prerequisites
- Docker installed and running
- Docker Compose v2 (`docker compose`)
## Files
- `Dockerfile`
- Multi-stage build that compiles the Go server (`./cmd/server`) into a small runtime image.
- `docker-compose.yml`
- Defines the `SSSData` service.
- Loads environment variables from `./.env`.
## Environment Variables
The application reads configuration from `.env` (not committed to git).
Common variables used by the server:
- `APP_PORT`
- `DB_HOST`
- `DB_PORT`
- `DB_USER`
- `DB_PASSWORD`
- `DB_NAME`
- `DB_SSLMODE` (defaults to `disable`)
- `ProviderUser`
- `ProviderSecret`
Notes:
- The container exposes port `3003`.
- If you change `APP_PORT`, ensure your `docker-compose.yml` port mapping matches.
## Run locally
From the repository root:
```bash
docker compose up -d --build
```
Endpoints:
- Health: `http://localhost:3003/health`
- Swagger: `http://localhost:3003/swagger/index.html`
View logs:
```bash
docker compose logs -f --tail=200
```
Stop:
```bash
docker compose down
```
## Run with Postgres in Docker (recommended for a VM / one-time install)
If you want the VM to have "everything in one place" (API + Postgres), use the `with-db` profile.
### 1) Set `.env` for the Docker Postgres
- Set `DB_HOST=postgres`
- Set `DB_PORT=5432`
- Set `DB_USER`, `DB_PASSWORD`, `DB_NAME`
The `postgres` container will use these values on first boot to create the database.
### 2) Start the full stack
```bash
docker compose --profile with-db up -d --build
```
This will create a persistent Docker volume named `sssdata_pgdata` to store your DB data.
### 3) Stop / reset
Stop containers:
```bash
docker compose down
```
If you want to delete the DB data (DANGER: wipes DB):
```bash
docker compose down -v
```
## Deploy to a VPS (build on VPS)
This is the "Option 2" flow: the VPS builds the image from source.
### First time
1. Clone the repo on the VPS
2. Create a `.env` file on the VPS (production values)
3. Build and start:
```bash
docker compose up -d --build
```
### Update / redeploy
```bash
git pull
docker compose up -d --build
```
If you need to force a clean rebuild (no cache):
```bash
docker compose build --no-cache
docker compose up -d
```
## DB host guidance (important)
This project connects to Postgres using `DB_HOST`.
- If Postgres runs as a container in the same `docker-compose.yml`, set `DB_HOST` to the Postgres service name (example: `postgres`).
- If Postgres runs outside Docker (on the host or managed DB), set `DB_HOST` to the correct reachable hostname/IP.
When using the built-in Docker Postgres service:
- run with: `docker compose --profile with-db up -d --build`
- set: `DB_HOST=postgres`
On Linux hosts, `host.docker.internal` may not be available by default.
If you need to connect from a container to a database on the host machine, you may need to:
- use the host IP address, or
- add an `extra_hosts` entry in `docker-compose.yml` (ask the sysadmin / adjust per environment).
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
basePath: /api
definitions:
handlers.AreaDTO:
properties:
alpha2code:
type: string
alpha3code:
type: string
id:
type: integer
name:
type: string
type: object
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.StandingListResponse:
properties:
data:
items:
$ref: '#/definitions/handlers.StandingOut'
type: array
meta:
additionalProperties: true
type: object
type: object
handlers.StandingOut:
properties:
apiLastSyncedAt:
type: string
apiSyncStatus:
type: string
competitionWyId:
type: integer
goalDifference:
type: integer
groupName:
type: string
pointsPerGame:
type: number
position:
type: integer
seasonWyId:
type: integer
team:
$ref: '#/definitions/handlers.StandingTeamOut'
teamWyId:
type: integer
totalDraws:
type: integer
totalGoalsAgainst:
type: integer
totalGoalsFor:
type: integer
totalLosses:
type: integer
totalPlayed:
type: integer
totalPoints:
type: integer
totalWins:
type: integer
type: object
handlers.StandingTeamOut:
properties:
id:
type: string
image:
type: string
name:
type: string
wyId:
type: integer
type: object
handlers.StructuredCoach:
properties:
apiLastSyncedAt:
type: string
apiSyncStatus:
type: string
area:
$ref: '#/definitions/handlers.AreaDTO'
coachingLicense:
type: string
contractUntil:
type: string
countryName:
type: string
countryTsId:
type: string
createdAt:
type: string
currentTeam:
$ref: '#/definitions/handlers.TeamDTO'
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.TeamDTO:
properties:
apiLastSyncedAt:
type: string
apiSyncStatus:
type: string
area:
$ref: '#/definitions/handlers.AreaDTO'
areaName:
type: string
areaWyId:
type: integer
category:
type: string
city:
type: string
createdAt:
type: string
deletedAt:
type: string
gender:
type: string
id:
type: string
imageDataUrl:
type: string
isActive:
type: boolean
name:
type: string
officialName:
type: string
status:
type: string
type:
type: string
updatedAt:
type: string
wyId:
type: integer
type: object
handlers.TeamImagesResponse:
properties:
data:
properties:
children:
items:
properties:
imageDataUrl:
type: string
name:
type: string
wyId:
type: integer
type: object
type: array
imageDataUrl:
type: string
wyId:
type: integer
type: object
meta:
additionalProperties: true
type: object
type: object
handlers.TeamListResponse:
properties:
data:
items:
$ref: '#/definitions/handlers.TeamDTO'
type: array
meta:
additionalProperties: true
type: object
type: object
handlers.TeamResponse:
properties:
data:
$ref: '#/definitions/handlers.TeamDTO'
meta:
additionalProperties: true
type: object
type: object
handlers.competitionGetResponse:
properties:
data:
$ref: '#/definitions/handlers.competitionResponse'
meta:
additionalProperties: true
type: object
type: object
handlers.competitionResponse:
properties:
area:
$ref: '#/definitions/handlers.AreaDTO'
areaWyId:
type: integer
category:
type: string
countryName:
type: string
countryTsId:
type: string
createdAt:
type: string
curRound:
type: integer
curSeasonTsId:
type: string
curStageTsId:
type: string
deletedAt:
type: string
divisions:
items:
type: object
type: array
gender:
type: string
host:
type: object
id:
type: string
isActive:
type: boolean
logo:
type: string
mostTitles:
items:
type: object
type: array
name:
type: string
newcomers:
items:
type: object
type: array
officialName:
type: string
roundCount:
type: integer
shortName:
type: string
theSportsUpdatedAt:
type: string
titleHolder:
items:
type: object
type: array
tsCategoryId:
type: string
tsId:
type: string
tsType:
type: integer
type:
type: string
uid:
type: string
updatedAt:
type: string
wyId:
type: integer
type: object
handlers.competitionsListResponse:
properties:
data:
items:
$ref: '#/definitions/handlers.competitionResponse'
type: array
meta:
additionalProperties: true
type: object
type: object
handlers.matchEntityOut:
properties:
name:
type: string
wyId:
type: integer
type: object
handlers.matchOut:
properties:
awayTeam:
$ref: '#/definitions/handlers.matchTeamOut'
competition:
$ref: '#/definitions/handlers.matchEntityOut'
homeTeam:
$ref: '#/definitions/handlers.matchTeamOut'
round:
$ref: '#/definitions/handlers.matchEntityOut'
season:
$ref: '#/definitions/handlers.matchEntityOut'
type: object
handlers.matchTeamOut:
properties:
id:
type: string
image:
type: string
name:
type: string
wyId:
type: integer
type: object
info:
contact: {}
description: API server for scouting system score data.
title: Scouting System Score Data API
version: "1.0"
paths:
/areas:
get:
description: Returns a paginated list of areas, optionally filtered by name
or alpha codes.
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
- description: Filter areas by name (case-insensitive, partial match)
in: query
name: name
type: string
- description: Filter areas by 2-letter country code
in: query
name: alpha2code
type: string
- description: Filter areas by 3-letter country code
in: query
name: alpha3code
type: string
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: List areas
tags:
- Areas
/areas/{id}:
get:
description: Returns a single area by its internal ID.
parameters:
- description: Area 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 area by ID
tags:
- Areas
/areas/provider/{providerId}:
get:
description: Returns a single area by its provider (wy_id) identifier.
parameters:
- description: Provider (wy_id) identifier
in: path
name: providerId
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 area by provider ID
tags:
- Areas
/areas/wyscout/{wyId}:
get:
description: Returns a single area 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 area by Wyscout wy_id
tags:
- Areas
/coaches:
get:
description: Returns a paginated list of coaches, optionally filtered by name,
team, position, or active status.
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
- description: Filter coaches by name (first, last, middle, or short)
in: query
name: name
type: string
- description: Filter coaches by current team ID (wy_id)
in: query
name: teamId
type: string
- description: Filter coaches by position (head_coach, assistant_coach, etc.)
in: query
name: position
type: string
- description: Filter active coaches only
in: query
name: active
type: boolean
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.CoachListResponse'
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: List coaches
tags:
- Coaches
/coaches/{id}:
get:
description: Returns a single coach by its internal ID.
parameters:
- description: Coach internal identifier
in: path
name: id
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.CoachResponse'
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get coach by ID
tags:
- Coaches
/coaches/provider/{providerId}:
get:
description: 'Returns a single coach by its provider identifier: numeric values
are treated as Wyscout wy_id, non-numeric as TheSports ts_id.'
parameters:
- description: Provider identifier (wy_id or ts_id)
in: path
name: providerId
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.CoachResponse'
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get coach by provider ID
tags:
- Coaches
/coaches/wyscout/{wyId}:
get:
description: Returns a single coach 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:
$ref: '#/definitions/handlers.CoachResponse'
"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 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
- description: Filter by competition name (case-insensitive, partial match)
in: query
name: name
type: string
- description: Filter by Wyscout area (country) wy_id (matches competitions.area_wy_id)
in: query
name: countryId
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.competitionsListResponse'
"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:
$ref: '#/definitions/handlers.competitionGetResponse'
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get competition by ID
tags:
- Competitions
/competitions/provider/{providerId}:
get:
description: Returns a single competition by provider ID. If providerId is numeric
it is treated as Wyscout wy_id, otherwise it is treated as TheSports ts_id.
parameters:
- description: Provider identifier (wy_id or ts_id)
in: path
name: providerId
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.competitionGetResponse'
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get competition by provider ID
tags:
- Competitions
/competitions/wyscout/{wyId}:
get:
description: Returns a single competition by its Wyscout wy_id identifier.
parameters:
- description: Wyscout wy_id identifier
in: path
name: wyId
required: true
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.competitionGetResponse'
"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
API.
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import areas from TheSports
tags:
- Import
/import/coaches:
post:
description: Imports coach profiles from Wyscout /v3/coaches/{id}. Discovers
coach IDs either from match_teams.coach_wy_id (recommended) or via team profiles.
parameters:
- description: 'Discovery source: matchTeams (recommended) or teams'
enum:
- matchTeams
- teams
in: query
name: source
type: string
- description: Limit number of coach profiles fetched
in: query
name: limitCoaches
type: integer
- description: Limit number of teams processed when source=teams
in: query
name: limitTeams
type: integer
- description: Only discover coach from a specific team (only used when source=teams)
in: query
name: teamWyId
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import coaches from Wyscout
tags:
- Import
/import/competitions:
post:
description: Performs a competition import using TheSports competition additional
list API. If `since` is provided (unix seconds), only competitions updated
since that time are fetched using the time-based endpoint. Otherwise, a full
import is performed using page-based pagination.
parameters:
- description: Page size per request (default 100, only used for full imports)
in: query
name: pageSize
type: integer
- description: Unix timestamp (seconds) to import only competitions updated
since this time
in: query
name: since
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import competitions from TheSports
tags:
- Import
/import/matches/diary:
post:
description: Performs a matches import using TheSports match/diary API for a
given date or 24h window. The `date` query parameter (YYYY-MM-DD) is recommended;
if omitted, the provider default will be used (usually current day).
parameters:
- description: Date in YYYY-MM-DD format for which to import the schedule/results
in: query
name: date
type: string
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import matches diary from TheSports
tags:
- Import
/import/matches/fixtures:
post:
description: Imports matches for a given Wyscout season via /v3/seasons/{seasonWyId}/fixtures?details=matches.
If seasonWyId is omitted, imports for all seasons in the DB with a wy_id.
parameters:
- description: Wyscout season ID
in: query
name: seasonWyId
type: integer
- description: Limit number of seasons processed when seasonWyId is omitted
in: query
name: limitSeasons
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import matches from Wyscout fixtures
tags:
- Import
/import/matches/formations:
post:
description: Fetches /v4/matches/{matchWyId}/formations for a given matchWyId.
If matchWyId is omitted, processes matches in the DB with a wy_id. Stores
formations in match_formations.
parameters:
- description: Wyscout match wy_id
in: query
name: matchWyId
type: integer
- description: Limit number of matches processed when matchWyId is omitted
in: query
name: limit
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import match formations from Wyscout v4
tags:
- Import
/import/matches/lineup:
post:
description: Performs a lineup import using TheSports match/lineup/detail API.
If 'matchTsId' is provided, imports lineup for a specific match. If omitted,
processes matches from the last 30 days only (API limitation). Use 'limit'
for testing and 'batchSize' to control memory usage.
parameters:
- description: TheSports match id (tsId) for which to import the lineup (optional;
if omitted, processes matches from last 30 days)
in: query
name: matchTsId
type: string
- description: 'Maximum number of matches to process in batch mode (default:
no limit; useful for debugging)'
in: query
name: limit
type: integer
- description: 'Number of matches to load per batch (default: 1000; lower for
memory constraints)'
in: query
name: batchSize
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import match lineups from TheSports
tags:
- Import
/import/matches/list:
post:
description: Performs a full import of all matches from TheSports match/list
API using pagination. This is intended for one-time initial sync to get all
historical matches. The API returns 1000 matches per page and stops when total
is 0. Use startPage to resume from a specific page if the import was interrupted.
parameters:
- description: 'Starting page number (default: 1, use to resume interrupted
import)'
in: query
name: startPage
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import all matches from TheSports (one-time full sync)
tags:
- Import
/import/matches/recent:
post:
description: Performs a matches import using TheSports match/recent/list API.
If `since` is provided (unix seconds), only matches updated since that time
are fetched using the time-based endpoint. Otherwise, a full import is performed
using page-based pagination (last 30 days).
parameters:
- description: Page size per request (default 100, only used for full imports)
in: query
name: pageSize
type: integer
- description: Unix timestamp (seconds) to import only matches updated since
this time
in: query
name: since
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import recent matches from TheSports
tags:
- Import
/import/matches/wyscout:
post:
description: Imports matches for a given Wyscout season via /v3/seasons/{seasonWyId}/matches.
Requires seasonWyId query param.
parameters:
- description: Wyscout season ID
in: query
name: seasonWyId
required: true
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import matches from Wyscout season matches
tags:
- Import
/import/players:
post:
description: Performs a player import using TheSports player with_stat list
API. If `since` is provided (unix seconds), only players updated since that
time are fetched using the time-based endpoint. Otherwise, a full import is
performed using page-based pagination.
parameters:
- description: Page size per request (default 100, only used for full imports)
in: query
name: pageSize
type: integer
- description: Unix timestamp (seconds) to import only players updated since
this time
in: query
name: since
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import players from TheSports
tags:
- Import
/import/referees:
post:
description: Imports referees either from Wyscout /v3/referees/{id} (recommended)
or legacy TheSports referee list.
parameters:
- description: 'Discovery source: matches (recommended) or theSports'
enum:
- matches
- theSports
in: query
name: source
type: string
- description: Limit number of referee profiles fetched (Wyscout mode)
in: query
name: limitReferees
type: integer
- description: Page size per request (default 100, only used for theSports mode)
in: query
name: pageSize
type: integer
- description: Unix timestamp (seconds) to import only referees updated since
this time (theSports mode)
in: query
name: since
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import referees
tags:
- Import
/import/seasons:
post:
description: Performs a season import using TheSports season list API. If `since`
is provided (unix seconds), only seasons updated since that time are fetched
using the time-based endpoint. Otherwise, a full import is performed using
page-based pagination.
parameters:
- description: Page size per request (default 100, only used for full imports)
in: query
name: pageSize
type: integer
- description: Unix timestamp (seconds) to import only seasons updated since
this time
in: query
name: since
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import seasons from TheSports
tags:
- Import
/import/standings:
post:
description: Imports season standings via Wyscout /v3/seasons/{seasonWyId}/standings.
If seasonWyId is omitted, imports standings for all seasons in the DB with
a wy_id. Use limit for testing.
parameters:
- description: Wyscout season ID
in: query
name: seasonWyId
type: integer
- description: Limit number of seasons processed when seasonWyId is omitted
in: query
name: limit
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import standings from Wyscout
tags:
- Import
/import/teams:
post:
description: Performs a team import using TheSports team additional list API.
If `since` is provided (unix seconds), only teams updated since that time
are fetched using the time-based endpoint. Otherwise, a full import is performed
using page-based pagination.
parameters:
- description: Page size per request (default 100, only used for full imports)
in: query
name: pageSize
type: integer
- description: Unix timestamp (seconds) to import only teams updated since this
time
in: query
name: since
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import teams from TheSports
tags:
- Import
/import/teams/images:
post:
description: Fetches /v3/teams/{wyId} for a specific team (or for teams in DB
when wyId is omitted) and stores imageDataURL on the team and its children.
parameters:
- description: Wyscout team wy_id to import
in: query
name: wyId
type: integer
- description: When wyId is omitted, only process teams with wy_id > afterWyId
(cursor)
in: query
name: afterWyId
type: integer
- description: Limit number of teams processed when wyId is omitted
in: query
name: limit
type: integer
- description: Only process teams missing imageDataUrl when wyId is omitted
(default true)
in: query
name: missingOnly
type: boolean
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import team imageDataURL from Wyscout
tags:
- Import
/matches:
get:
description: Returns a paginated list of matches, optionally filtered by competitionWyId,
seasonWyId, teamTsId/teamWyId (+teamSide) and date range.
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
- description: Filter by competition wy_id
in: query
name: competitionWyId
type: integer
- description: Filter by season wy_id
in: query
name: seasonWyId
type: integer
- description: Filter by team ts_id (matches where team is home or away)
in: query
name: teamTsId
type: string
- description: Filter by team wy_id (matches where team is home or away)
in: query
name: teamWyId
type: integer
- description: 'When filtering by teamTsId/teamWyId, restrict side: home|away|either
(default either)'
in: query
name: teamSide
type: string
- description: Filter matches on/after date (RFC3339 or YYYY-MM-DD)
in: query
name: from
type: string
- description: Filter matches on/before date (RFC3339 or YYYY-MM-DD)
in: query
name: to
type: string
- description: Filter by match status
in: query
name: status
type: string
- description: 'Sort order by match_date: asc|desc (default desc)'
in: query
name: order
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.MatchListResponse'
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: List matches
tags:
- Matches
/matches/{matchTsId}/lineup:
get:
description: Returns the stored lineup data for a match (teams/formations/coaches
+ players) by match ts_id.
parameters:
- description: TheSports match id (tsId)
in: path
name: matchTsId
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 match lineup by match id
tags:
- Matches
/matches/head-to-head:
get:
description: Returns a paginated list of matches where teamHomeWyId played at
home and teamAwayWyId played away (by wy_id).
parameters:
- description: Home team wy_id
in: query
name: teamHomeWyId
required: true
type: integer
- description: Away team wy_id
in: query
name: teamAwayWyId
required: true
type: integer
- description: Maximum number of items to return (default 100)
in: query
name: limit
type: integer
- description: Number of items to skip before starting to collect the result
set (default 0)
in: query
name: offset
type: integer
- description: Filter matches on/after date (RFC3339 or YYYY-MM-DD)
in: query
name: from
type: string
- description: Filter matches on/before date (RFC3339 or YYYY-MM-DD)
in: query
name: to
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.MatchListResponse'
"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: List matches between two teams
tags:
- Matches
/matches/wyscout/{wyId}:
get:
description: Returns a match by its Wyscout wy_id.
parameters:
- description: Wyscout wy_id identifier
in: path
name: wyId
required: true
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.MatchResponse'
"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 match by Wyscout match wy_id
tags:
- Matches
/matches/wyscout/{wyId}/lineup:
get:
description: Returns the stored lineup data for a match (teams/formations/coaches
+ players) by match wy_id.
parameters:
- description: Wyscout wy_id identifier
in: path
name: wyId
required: true
type: integer
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 match lineup by Wyscout match wy_id
tags:
- Matches
/players:
get:
description: Returns a paginated list of players, optionally filtered by name,
team, or country.
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
- description: Filter players by name (short, first, middle, or last)
in: query
name: name
type: string
- description: Filter players by current team ID
in: query
name: teamId
type: string
- description: Filter players by birth country name
in: query
name: country
type: string
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: List players
tags:
- Players
/players/{id}:
get:
description: Returns a single player by its internal ID.
parameters:
- description: Player 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 player by ID
tags:
- Players
/players/wyscout/{wyId}:
get:
description: Returns a single player by its provider (wy_id) identifier.
parameters:
- description: Provider identifier (wy_id numeric or ts_id string)
in: path
name: wyId
required: true
type: string
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 player by provider ID
tags:
- Players
/referees:
get:
description: Returns a paginated list of referees, optionally filtered by name,
referee type, nationality, or active status.
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
- description: Filter referees by name (first, last, middle, or short)
in: query
name: name
type: string
- description: Filter referees by nationality WyID
in: query
name: countryWyId
type: integer
- description: Filter referees by type (main, assistant, var, etc.)
in: query
name: type
type: string
- description: Filter active referees only
in: query
name: active
type: boolean
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: List referees
tags:
- Referees
/referees/{id}:
get:
description: Returns a single referee by its internal ID.
parameters:
- description: Referee 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 referee by ID
tags:
- Referees
/referees/provider/{providerId}:
get:
description: 'Returns a single referee by its provider identifier: numeric values
are treated as Wyscout wy_id, non-numeric as TheSports ts_id.'
parameters:
- description: Provider identifier (wy_id or ts_id)
in: path
name: providerId
required: true
type: string
responses:
"200":
description: OK
schema:
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 referee by provider ID
tags:
- Referees
/referees/wyscout/{wyId}:
get:
description: Returns a single referee 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 referee by Wyscout ID
tags:
- Referees
/seasons:
get:
description: Returns a list of seasons 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
- description: Filter by season name (case-insensitive, partial match)
in: query
name: name
type: string
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"500":
description: Internal Server Error
schema:
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
/standings:
get:
description: Returns a paginated list of standings. Provide seasonWyId or competitionWyId
to scope results.
parameters:
- description: Filter by season wy_id
in: query
name: seasonWyId
type: integer
- description: Filter by competition wy_id
in: query
name: competitionWyId
type: integer
- description: Filter by team wy_id
in: query
name: teamWyId
type: integer
- description: Filter by group name (exact match)
in: query
name: groupName
type: string
- description: Maximum number of items to return (default 100)
in: query
name: limit
type: integer
- description: Number of items to skip before starting to collect the result
set (default 0)
in: query
name: offset
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.StandingListResponse'
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: List standings
tags:
- Standings
/standings/season/{seasonWyId}:
get:
description: Convenience endpoint to list standings for a single season.
parameters:
- description: Wyscout season wy_id
in: path
name: seasonWyId
required: true
type: integer
- description: Optional competition wy_id filter
in: query
name: competitionWyId
type: integer
- description: Optional team wy_id filter
in: query
name: teamWyId
type: integer
- description: Filter by group name (exact match)
in: query
name: groupName
type: string
- description: Maximum number of items to return (default 100)
in: query
name: limit
type: integer
- description: Number of items to skip before starting to collect the result
set (default 0)
in: query
name: offset
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.StandingListResponse'
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: List standings by season
tags:
- Standings
/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
/teams/{id}:
get:
description: Returns a single team by its internal ID.
parameters:
- description: Team internal identifier
in: path
name: id
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.TeamResponse'
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get team by ID
tags:
- Teams
/teams/{id}/images:
get:
description: Returns imageDataUrl for a given team id and its child teams.
parameters:
- description: Team internal identifier
in: path
name: id
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.TeamImagesResponse'
"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 team images by internal ID
tags:
- Teams
/teams/wyscout/{wyId}:
get:
description: Returns a single team 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:
$ref: '#/definitions/handlers.TeamResponse'
"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 team by Wyscout ID
tags:
- Teams
/teams/wyscout/{wyId}/images:
get:
description: Returns imageDataUrl for a given team wyId and its child teams.
parameters:
- description: Wyscout wy_id identifier
in: path
name: wyId
required: true
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.TeamImagesResponse'
"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 team images by Wyscout ID
tags:
- Teams
/teams/wyscout/{wyId}/players:
get:
description: Returns players for a given team wy_id.
parameters:
- description: Wyscout wy_id identifier
in: path
name: wyId
required: true
type: integer
- 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
"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: List players by team Wyscout ID
tags:
- Teams
swagger: "2.0"
module.exports = {
apps: [
{
name: "sssdata",
cwd: __dirname,
script: "./server",
interpreter: "none",
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: "300M",
time: true,
env: {
APP_PORT: process.env.APP_PORT || "3003",
DB_HOST: process.env.DB_HOST,
DB_PORT: process.env.DB_PORT,
DB_USER: process.env.DB_USER,
DB_PASSWORD: process.env.DB_PASSWORD,
DB_NAME: process.env.DB_NAME,
DB_SSLMODE: process.env.DB_SSLMODE,
ProviderUser: process.env.ProviderUser,
ProviderSecret: process.env.ProviderSecret,
API_USERNAME: process.env.API_USERNAME,
API_PASSWORD: process.env.API_PASSWORD,
},
},
],
};
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
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE=
github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
package config
import "os"
type Config struct {
Port string
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
SSLMode string
ProviderUser string
ProviderSecret string
}
func Load() Config {
return Config{
Port: os.Getenv("APP_PORT"),
DBHost: os.Getenv("DB_HOST"),
DBPort: os.Getenv("DB_PORT"),
DBUser: os.Getenv("DB_USER"),
DBPassword: os.Getenv("DB_PASSWORD"),
DBName: os.Getenv("DB_NAME"),
SSLMode: envOrDefault("DB_SSLMODE", "disable"),
ProviderUser: os.Getenv("ProviderUser"),
ProviderSecret: os.Getenv("ProviderSecret"),
}
}
func envOrDefault(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
package database
import (
"fmt"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/config"
"ScoutingSystemScoreData/internal/models"
)
func Connect(cfg config.Config) (*gorm.DB, error) {
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
cfg.DBHost,
cfg.DBPort,
cfg.DBUser,
cfg.DBPassword,
cfg.DBName,
cfg.SSLMode,
)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
if err := db.AutoMigrate(
&models.Area{},
&models.Competition{},
&models.Season{},
&models.Round{},
&models.Team{},
&models.TeamChild{},
&models.Coach{},
&models.Referee{},
&models.Player{},
&models.Match{},
&models.MatchTeam{},
&models.MatchLineupPlayer{},
&models.MatchFormation{},
&models.PlayerTransfer{},
&models.TeamSquad{},
&models.Standing{},
&models.SampleRecord{},
); err != nil {
return nil, err
}
return db, nil
}
package errors
import "errors"
type Code string
const (
CodeNotFound Code = "NOT_FOUND"
CodeInvalidInput Code = "INVALID_INPUT"
CodeConflict Code = "CONFLICT"
CodeInternal Code = "INTERNAL"
)
type AppError struct {
Code Code
Message string
Err error
}
func (e *AppError) Error() string {
return e.Message
}
func Wrap(code Code, msg string, err error) *AppError {
return &AppError{Code: code, Message: msg, Err: err}
}
func New(code Code, msg string) *AppError {
return &AppError{Code: code, Message: msg}
}
func IsCode(err error, code Code) bool {
var ae *AppError
if errors.As(err, &ae) {
return ae.Code == code
}
return false
}
package handlers
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/services"
)
type AreaHandler struct {
Service services.AreaService
}
func RegisterAreaRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewAreaService(db)
h := &AreaHandler{Service: service}
areas := rg.Group("/areas")
areas.GET("", h.List)
areas.GET("/:id", h.GetByID)
areas.GET("/provider/:providerId", h.GetByProviderID)
areas.GET("/wyscout/:wyId", h.GetByWyID)
}
// List areas
// @Summary List areas
// @Description Returns a paginated list of areas, optionally filtered by name or alpha codes.
// @Tags Areas
// @Param limit query int false "Maximum number of items to return (default 100)"
// @Param offset query int false "Number of items to skip before starting to collect the result set (default 0)"
// @Param name query string false "Filter areas by name (case-insensitive, partial match)"
// @Param alpha2code query string false "Filter areas by 2-letter country code"
// @Param alpha3code query string false "Filter areas by 3-letter country code"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Router /areas [get]
func (h *AreaHandler) 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
}
name := c.Query("name")
alpha2 := c.Query("alpha2code")
alpha3 := c.Query("alpha3code")
endpoint := "/areas"
if name != "" {
endpoint = fmt.Sprintf("/areas?name=%s", name)
} else if alpha2 != "" {
endpoint = fmt.Sprintf("/areas?alpha2code=%s", alpha2)
} else if alpha3 != "" {
endpoint = fmt.Sprintf("/areas?alpha3code=%s", alpha3)
}
areas, total, err := h.Service.ListAreas(c.Request.Context(), services.ListAreasOptions{
Limit: limit,
Offset: offset,
Name: name,
Alpha2: alpha2,
Alpha3: alpha3,
})
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": areas,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
"totalItems": total,
"page": page,
"limit": limit,
"hasMore": hasMore,
},
})
}
// GetByWyID returns a single area by Wyscout wy_id
// @Summary Get area by Wyscout wy_id
// @Description Returns a single area by its Wyscout wy_id identifier.
// @Tags Areas
// @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 /areas/wyscout/{wyId} [get]
func (h *AreaHandler) GetByWyID(c *gin.Context) {
wyIDStr := c.Param("wyId")
wyID, err := strconv.Atoi(wyIDStr)
if err != nil || wyID <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid wyId"})
return
}
area, err := h.Service.GetByProviderID(c.Request.Context(), strconv.Itoa(wyID))
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/areas/wyscout/%d", wyID)
c.JSON(http.StatusOK, gin.H{
"data": area,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByID returns a single area by internal ID
// @Summary Get area by ID
// @Description Returns a single area by its internal ID.
// @Tags Areas
// @Param id path string true "Area internal identifier"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /areas/{id} [get]
func (h *AreaHandler) GetByID(c *gin.Context) {
id := c.Param("id")
area, 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("/areas/%s", id)
c.JSON(http.StatusOK, gin.H{
"data": area,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByProviderID returns a single area by wy_id provider ID
// @Summary Get area by provider ID
// @Description Returns a single area by its provider (wy_id) identifier.
// @Tags Areas
// @Param providerId path string true "Provider (wy_id) identifier"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /areas/provider/{providerId} [get]
func (h *AreaHandler) GetByProviderID(c *gin.Context) {
providerID := c.Param("providerId")
area, err := h.Service.GetByProviderID(c.Request.Context(), providerID)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/areas/provider/%s", providerID)
c.JSON(http.StatusOK, gin.H{
"data": area,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
package handlers
import (
"context"
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/models"
"ScoutingSystemScoreData/internal/services"
)
type CoachHandler struct {
DB *gorm.DB
Service services.CoachService
Teams services.TeamService
Areas services.AreaService
}
func RegisterCoachRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewCoachService(db)
teamService := services.NewTeamService(db)
areaService := services.NewAreaService(db)
h := &CoachHandler{DB: db, Service: service, Teams: teamService, Areas: areaService}
coaches := rg.Group("/coaches")
coaches.GET("", h.List)
coaches.GET("/wyscout/:wyId", h.GetByWyID)
coaches.GET("/provider/:providerId", h.GetByProviderID)
coaches.GET("/:id", h.GetByID)
}
type StructuredCoach struct {
ID string `json:"id"`
WyID *int `json:"wyId"`
TsID string `json:"tsId"`
CountryTsID *string `json:"countryTsId"`
CountryName *string `json:"countryName"`
Area *AreaDTO `json:"area"`
CurrentTeam *TeamDTO `json:"currentTeam"`
TeamTsID *string `json:"teamTsId"`
TeamName *string `json:"teamName"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
MiddleName *string `json:"middleName"`
ShortName *string `json:"shortName"`
DateOfBirth *time.Time `json:"dateOfBirth"`
NationalityWyID *int `json:"nationalityWyId"`
CurrentTeamWyID *int `json:"currentTeamWyId"`
Position string `json:"position"`
CoachingLicense *string `json:"coachingLicense"`
YearsExperience *int `json:"yearsExperience"`
PreferredFormation *string `json:"preferredFormation"`
JoinedAt *time.Time `json:"joinedAt"`
ContractUntil *time.Time `json:"contractUntil"`
UID *string `json:"uid"`
Deathday *time.Time `json:"deathday"`
Status string `json:"status"`
ImageDataURL *string `json:"imageDataUrl"`
APILastSyncedAt *time.Time `json:"apiLastSyncedAt"`
APISyncStatus string `json:"apiSyncStatus"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"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))
currentTeamWyIDs := make([]int, 0, len(coaches))
seenArea := map[string]struct{}{}
seenTeam := map[string]struct{}{}
seenTeamWy := map[int]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)
}
}
if c.CurrentTeamWyID != nil {
wy := *c.CurrentTeamWyID
if wy > 0 {
if _, ok := seenTeamWy[wy]; !ok {
seenTeamWy[wy] = struct{}{}
currentTeamWyIDs = append(currentTeamWyIDs, wy)
}
}
}
}
areaNameByTsID := map[string]string{}
areaByTsID := map[string]models.Area{}
if len(areaTsIDs) > 0 {
var areas []models.Area
if err := h.DB.WithContext(ctx).
Model(&models.Area{}).
Select("ts_id", "wy_id", "alpha2code", "alpha3code", "name").
Where("ts_id IN ?", areaTsIDs).
Find(&areas).Error; err != nil {
return nil, err
}
for _, a := range areas {
if a.TsID != nil && *a.TsID != "" {
areaNameByTsID[*a.TsID] = a.Name
areaByTsID[*a.TsID] = a
}
}
}
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
}
}
}
teamsByWyID := map[int]models.Team{}
areasByWyID := map[int]models.Area{}
if len(currentTeamWyIDs) > 0 {
teams, err := h.Teams.GetByWyIDs(ctx, currentTeamWyIDs)
if err != nil {
return nil, err
}
areaWyIDs := make([]int, 0, len(teams))
seenAreaWy := map[int]struct{}{}
for _, t := range teams {
if t.WyID != nil {
teamsByWyID[*t.WyID] = t
}
if t.AreaWyID != nil {
awy := *t.AreaWyID
if awy > 0 {
if _, ok := seenAreaWy[awy]; !ok {
seenAreaWy[awy] = struct{}{}
areaWyIDs = append(areaWyIDs, awy)
}
}
}
}
if len(areaWyIDs) > 0 {
areas, err := h.Areas.GetByWyIDs(ctx, areaWyIDs)
if err != nil {
return nil, err
}
for _, a := range areas {
wyStr := a.WyID
if wyStr == "" {
continue
}
wy, err := strconv.Atoi(wyStr)
if err != nil {
continue
}
areasByWyID[wy] = a
}
}
}
out := make([]StructuredCoach, 0, len(coaches))
for _, c := range coaches {
tsID := ""
if c.TsID != nil {
tsID = *c.TsID
}
var countryName *string
var areaDTO *AreaDTO
if c.CountryTsID != nil {
if n, ok := areaNameByTsID[*c.CountryTsID]; ok {
nn := n
countryName = &nn
}
if a, ok := areaByTsID[*c.CountryTsID]; ok {
dto := toAreaDTO(a)
areaDTO = &dto
}
}
var teamName *string
if c.TeamTsID != nil {
if n, ok := teamNameByTsID[*c.TeamTsID]; ok {
nn := n
teamName = &nn
}
}
var currentTeam *TeamDTO
if c.CurrentTeamWyID != nil {
if t, ok := teamsByWyID[*c.CurrentTeamWyID]; ok {
var teamAreaName *string
var teamAreaDTO *AreaDTO
if t.AreaWyID != nil {
if a, ok := areasByWyID[*t.AreaWyID]; ok {
name := a.Name
teamAreaName = &name
d := toAreaDTO(a)
teamAreaDTO = &d
}
}
dto := toTeamDTO(t, teamAreaName, teamAreaDTO)
currentTeam = &dto
}
}
out = append(out, StructuredCoach{
ID: c.ID,
WyID: nilIfZero(c.WyID),
TsID: tsID,
CountryTsID: c.CountryTsID,
CountryName: countryName,
Area: areaDTO,
CurrentTeam: currentTeam,
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 {
if v == nil {
return nil
}
if *v == 0 {
return nil
}
return v
}
// List coaches
// @Summary List coaches
// @Description Returns a paginated list of coaches, optionally filtered by name, team, position, or active status.
// @Tags Coaches
// @Param limit query int false "Maximum number of items to return (default 100)"
// @Param offset query int false "Number of items to skip before starting to collect the result set (default 0)"
// @Param name query string false "Filter coaches by name (first, last, middle, or short)"
// @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} handlers.CoachListResponse
// @Failure 500 {object} map[string]string
// @Router /coaches [get]
func (h *CoachHandler) 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
}
name := c.Query("name")
teamID := c.Query("teamId")
position := c.Query("position")
activeStr := c.Query("active")
activeOnly := activeStr == "true" || activeStr == "1"
endpoint := "/coaches"
if name != "" {
endpoint = fmt.Sprintf("/coaches?name=%s", name)
} else if teamID != "" {
endpoint = fmt.Sprintf("/coaches?teamId=%s", teamID)
} else if position != "" {
endpoint = fmt.Sprintf("/coaches?position=%s", position)
} else if activeOnly {
endpoint = "/coaches?active=true"
}
coaches, total, err := h.Service.ListCoaches(c.Request.Context(), services.ListCoachesOptions{
Limit: limit,
Offset: offset,
Name: name,
TeamID: teamID,
Position: position,
ActiveOnly: activeOnly,
})
if err != nil {
respondError(c, err)
return
}
structured, err := h.enrichCoaches(c.Request.Context(), coaches)
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": structured,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
"totalItems": total,
"page": page,
"limit": limit,
"hasMore": hasMore,
},
})
}
// GetByID returns a single coach by internal ID
// @Summary Get coach by ID
// @Description Returns a single coach by its internal ID.
// @Tags Coaches
// @Param id path string true "Coach internal identifier"
// @Success 200 {object} handlers.CoachResponse
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /coaches/{id} [get]
func (h *CoachHandler) GetByID(c *gin.Context) {
id := c.Param("id")
coach, err := h.Service.GetByID(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
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)
c.JSON(http.StatusOK, gin.H{
"data": structured,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByProviderID returns a single coach by wy_id (numeric) or ts_id (string)
// @Summary Get coach by provider ID
// @Description Returns a single coach by its provider identifier: numeric values are treated as Wyscout wy_id, non-numeric as TheSports ts_id.
// @Tags Coaches
// @Param providerId path string true "Provider identifier (wy_id or ts_id)"
// @Success 200 {object} handlers.CoachResponse
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /coaches/provider/{providerId} [get]
func (h *CoachHandler) GetByProviderID(c *gin.Context) {
providerID := c.Param("providerId")
coach, err := h.Service.GetByAnyProviderID(c.Request.Context(), providerID)
if err != nil {
respondError(c, err)
return
}
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)
c.JSON(http.StatusOK, gin.H{
"data": structured,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByWyID returns a single coach by WyScout WyID
// @Summary Get coach by Wyscout ID
// @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} handlers.CoachResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /coaches/wyscout/{wyId} [get]
func (h *CoachHandler) 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
}
coach, err := h.Service.GetByWyID(c.Request.Context(), wyID)
if err != nil {
respondError(c, err)
return
}
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)
c.JSON(http.StatusOK, gin.H{
"data": structured,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
package handlers
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/models"
"ScoutingSystemScoreData/internal/services"
)
type competitionResponse struct {
models.Competition
CountryName *string `json:"countryName"`
Area *AreaDTO `json:"area"`
}
type competitionsListResponse struct {
Data []competitionResponse `json:"data"`
Meta map[string]interface{} `json:"meta"`
}
type competitionGetResponse struct {
Data competitionResponse `json:"data"`
Meta map[string]interface{} `json:"meta"`
}
func lookupAreasByTsID(db *gorm.DB, countryTsIDs []string) (map[string]models.Area, error) {
if len(countryTsIDs) == 0 {
return map[string]models.Area{}, nil
}
var areas []models.Area
if err := db.Select("ts_id", "wy_id", "alpha2code", "alpha3code", "name").Where("ts_id IN ?", countryTsIDs).Find(&areas).Error; err != nil {
return nil, err
}
res := make(map[string]models.Area, len(areas))
for _, a := range areas {
if a.TsID != nil && *a.TsID != "" {
res[*a.TsID] = a
}
}
return res, nil
}
func lookupAreasByWyID(db *gorm.DB, wyIDs []int) (map[int]models.Area, error) {
if len(wyIDs) == 0 {
return map[int]models.Area{}, nil
}
filtered := make([]int, 0, len(wyIDs))
seen := map[int]struct{}{}
for _, id := range wyIDs {
if id <= 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
filtered = append(filtered, id)
}
if len(filtered) == 0 {
return map[int]models.Area{}, nil
}
wyIDStrs := make([]string, 0, len(filtered))
for _, id := range filtered {
wyIDStrs = append(wyIDStrs, strconv.Itoa(id))
}
var areas []models.Area
if err := db.Select("ts_id", "wy_id", "alpha2code", "alpha3code", "name").Where("wy_id IN ?", wyIDStrs).Find(&areas).Error; err != nil {
return nil, err
}
res := make(map[int]models.Area, len(areas))
for _, a := range areas {
if a.WyID == "" {
continue
}
id, err := strconv.Atoi(a.WyID)
if err != nil || id <= 0 {
continue
}
res[id] = a
}
return res, nil
}
type CompetitionHandler struct {
Service services.CompetitionService
DB *gorm.DB
}
func RegisterCompetitionRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewCompetitionService(db)
h := &CompetitionHandler{Service: service, DB: db}
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)"
// @Param name query string false "Filter by competition name (case-insensitive, partial match)"
// @Param countryId query int false "Filter by Wyscout area (country) wy_id (matches competitions.area_wy_id)"
// @Success 200 {object} competitionsListResponse
// @Failure 500 {object} map[string]string
// @Router /competitions [get]
func (h *CompetitionHandler) List(c *gin.Context) {
limitStr := c.DefaultQuery("limit", "100")
offsetStr := c.DefaultQuery("offset", "0")
nameStr := c.Query("name")
countryIDStr := c.Query("countryId")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
limit = 100
}
offset, err := strconv.Atoi(offsetStr)
if err != nil || offset < 0 {
offset = 0
}
var name *string
if nameStr != "" {
name = &nameStr
}
var areaWyID *int
if countryIDStr != "" {
if n, err := strconv.Atoi(countryIDStr); err == nil && n > 0 {
areaWyID = &n
}
}
competitions, total, err := h.Service.ListCompetitions(c.Request.Context(), limit, offset, name, areaWyID)
if err != nil {
respondError(c, err)
return
}
areaWyIDs := make([]int, 0, len(competitions))
for _, comp := range competitions {
if comp.AreaWyID != nil && *comp.AreaWyID > 0 {
areaWyIDs = append(areaWyIDs, *comp.AreaWyID)
}
}
areasByWyID, err := lookupAreasByWyID(h.DB, areaWyIDs)
_ = err
// Backwards-compatible enrichment fallback (older rows may only have country_ts_id).
countryTsIDs := make([]string, 0, len(competitions))
seenTs := make(map[string]struct{}, len(competitions))
for _, comp := range competitions {
if comp.CountryTsID != nil && *comp.CountryTsID != "" {
if _, ok := seenTs[*comp.CountryTsID]; !ok {
seenTs[*comp.CountryTsID] = struct{}{}
countryTsIDs = append(countryTsIDs, *comp.CountryTsID)
}
}
}
areasByTsID, err := lookupAreasByTsID(h.DB, countryTsIDs)
_ = err
data := make([]competitionResponse, 0, len(competitions))
for _, comp := range competitions {
var countryName *string
var areaDTO *AreaDTO
if comp.AreaWyID != nil {
if a, ok := areasByWyID[*comp.AreaWyID]; ok {
v := a.Name
countryName = &v
dto := toAreaDTO(a)
areaDTO = &dto
}
} else if comp.CountryTsID != nil {
if a, ok := areasByTsID[*comp.CountryTsID]; ok {
v := a.Name
countryName = &v
dto := toAreaDTO(a)
areaDTO = &dto
}
}
data = append(data, competitionResponse{Competition: comp, CountryName: countryName, Area: areaDTO})
}
page := offset/limit + 1
hasMore := int64(offset+limit) < total
timestamp := time.Now().UTC().Format(time.RFC3339)
c.JSON(http.StatusOK, gin.H{
"data": data,
"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} competitionGetResponse
// @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
}
var countryName *string
var areaDTO *AreaDTO
if comp.CountryTsID != nil && *comp.CountryTsID != "" {
var area models.Area
if err := h.DB.Select("wy_id", "alpha2code", "alpha3code", "name").Where("ts_id = ?", *comp.CountryTsID).First(&area).Error; err == nil {
v := area.Name
countryName = &v
dto := toAreaDTO(area)
areaDTO = &dto
}
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/competitions/%s", id)
c.JSON(http.StatusOK, gin.H{
"data": competitionResponse{Competition: comp, CountryName: countryName, Area: areaDTO},
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByProviderID returns a single competition by wy_id (numeric) or ts_id (string)
// @Summary Get competition by provider ID
// @Description Returns a single competition by provider ID. If providerId is numeric it is treated as Wyscout wy_id, otherwise it is treated as TheSports ts_id.
// @Tags Competitions
// @Param providerId path string true "Provider identifier (wy_id or ts_id)"
// @Success 200 {object} competitionGetResponse
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /competitions/provider/{providerId} [get]
func (h *CompetitionHandler) GetByProviderID(c *gin.Context) {
providerID := c.Param("providerId")
comp, err := h.Service.GetByAnyProviderID(c.Request.Context(), providerID)
if err != nil {
respondError(c, err)
return
}
var countryName *string
var areaDTO *AreaDTO
if comp.CountryTsID != nil && *comp.CountryTsID != "" {
var area models.Area
if err := h.DB.Select("wy_id", "alpha2code", "alpha3code", "name").Where("ts_id = ?", *comp.CountryTsID).First(&area).Error; err == nil {
v := area.Name
countryName = &v
dto := toAreaDTO(area)
areaDTO = &dto
}
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/competitions/provider/%s", providerID)
c.JSON(http.StatusOK, gin.H{
"data": competitionResponse{Competition: comp, CountryName: countryName, Area: areaDTO},
"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} competitionGetResponse
// @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
}
var countryName *string
var areaDTO *AreaDTO
if comp.CountryTsID != nil && *comp.CountryTsID != "" {
var area models.Area
if err := h.DB.Select("wy_id", "alpha2code", "alpha3code", "name").Where("ts_id = ?", *comp.CountryTsID).First(&area).Error; err == nil {
v := area.Name
countryName = &v
dto := toAreaDTO(area)
areaDTO = &dto
}
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/competitions/wyscout/%d", wyID)
c.JSON(http.StatusOK, gin.H{
"data": competitionResponse{Competition: comp, CountryName: countryName, Area: areaDTO},
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
appErrors "ScoutingSystemScoreData/internal/errors"
)
func respondError(c *gin.Context, err error) {
if appErrors.IsCode(err, appErrors.CodeNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
if appErrors.IsCode(err, appErrors.CodeInvalidInput) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
}
This source diff could not be displayed because it is too large. You can view the blob instead.
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/models"
"ScoutingSystemScoreData/internal/services"
)
type MatchHandler struct {
DB *gorm.DB
Service services.MatchService
}
func RegisterMatchRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewMatchService(db)
h := &MatchHandler{DB: db, Service: service}
matches := rg.Group("/matches")
matches.GET("", h.List)
matches.GET("/head-to-head", h.HeadToHeadMostRecent)
matches.GET("/wyscout/:wyId", h.GetByMatchWyID)
matches.GET("/wyscout/:wyId/lineup", h.GetLineupByMatchWyID)
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 matchTeamOut struct {
WyID *int `json:"wyId,omitempty"`
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
Image *string `json:"image,omitempty"`
}
type matchEntityOut struct {
WyID *int `json:"wyId,omitempty"`
Name *string `json:"name,omitempty"`
}
type matchOut struct {
Match models.Match `json:"-"`
HomeTeam *matchTeamOut `json:"homeTeam,omitempty"`
AwayTeam *matchTeamOut `json:"awayTeam,omitempty"`
Season *matchEntityOut `json:"season,omitempty"`
Round *matchEntityOut `json:"round,omitempty"`
Competition *matchEntityOut `json:"competition,omitempty"`
}
func (m matchOut) MarshalJSON() ([]byte, error) {
baseRaw, err := json.Marshal(m.Match)
if err != nil {
return nil, err
}
var base map[string]interface{}
if err := json.Unmarshal(baseRaw, &base); err != nil {
return nil, err
}
// Remove duplicated flat keys that will now live under nested objects.
delete(base, "homeTeamWyId")
delete(base, "awayTeamWyId")
delete(base, "seasonWyId")
delete(base, "roundWyId")
delete(base, "competitionWyId")
// Remove unneeded fields from match output.
delete(base, "attendance")
delete(base, "awayPosition")
delete(base, "awayTeamTsId")
delete(base, "competitionTsId")
delete(base, "coverageLineup")
delete(base, "coverageMlive")
delete(base, "hasOt")
delete(base, "homePosition")
delete(base, "homeTeamTsId")
delete(base, "humidity")
delete(base, "loss")
delete(base, "neutral")
delete(base, "notes")
delete(base, "pressure")
delete(base, "refereeTsId")
delete(base, "relatedTsId")
delete(base, "roundGroupNum")
delete(base, "roundStageTsId")
delete(base, "seasonTsId")
delete(base, "statusId")
delete(base, "tbd")
delete(base, "teamReverse")
delete(base, "temperature")
delete(base, "tsId")
delete(base, "venue")
delete(base, "venueCity")
delete(base, "venueCountry")
delete(base, "venueTsId")
delete(base, "weather")
delete(base, "wind")
if m.HomeTeam != nil {
base["homeTeam"] = m.HomeTeam
}
if m.AwayTeam != nil {
base["awayTeam"] = m.AwayTeam
}
if m.Season != nil {
base["season"] = m.Season
}
if m.Round != nil {
base["round"] = m.Round
}
if m.Competition != nil {
base["competition"] = m.Competition
}
return json.Marshal(base)
}
func (h *MatchHandler) enrichMatches(ctx context.Context, matches []models.Match) ([]matchOut, error) {
matchTsIDsMissingID := make([]string, 0, len(matches))
competitionWyIDs := make([]int, 0, len(matches))
seasonWyIDs := make([]int, 0, len(matches))
teamWyIDs := make([]int, 0, len(matches)*2)
roundWyIDs := make([]int, 0, len(matches))
seenComp := map[int]struct{}{}
seenSeason := map[int]struct{}{}
seenTeam := map[int]struct{}{}
seenRound := map[int]struct{}{}
for _, m := range matches {
if m.ID == "" && m.TsID != nil && *m.TsID != "" {
matchTsIDsMissingID = append(matchTsIDsMissingID, *m.TsID)
}
if m.CompetitionWyID != nil {
if _, ok := seenComp[*m.CompetitionWyID]; !ok {
seenComp[*m.CompetitionWyID] = struct{}{}
competitionWyIDs = append(competitionWyIDs, *m.CompetitionWyID)
}
}
if m.SeasonWyID != nil {
if _, ok := seenSeason[*m.SeasonWyID]; !ok {
seenSeason[*m.SeasonWyID] = struct{}{}
seasonWyIDs = append(seasonWyIDs, *m.SeasonWyID)
}
}
if m.RoundWyID != nil {
if _, ok := seenRound[*m.RoundWyID]; !ok {
seenRound[*m.RoundWyID] = struct{}{}
roundWyIDs = append(roundWyIDs, *m.RoundWyID)
}
}
if m.HomeTeamWyID != nil {
if _, ok := seenTeam[*m.HomeTeamWyID]; !ok {
seenTeam[*m.HomeTeamWyID] = struct{}{}
teamWyIDs = append(teamWyIDs, *m.HomeTeamWyID)
}
}
if m.AwayTeamWyID != nil {
if _, ok := seenTeam[*m.AwayTeamWyID]; !ok {
seenTeam[*m.AwayTeamWyID] = struct{}{}
teamWyIDs = append(teamWyIDs, *m.AwayTeamWyID)
}
}
}
competitionByWyID := map[int]string{}
if len(competitionWyIDs) > 0 {
var comps []struct {
WyID int `gorm:"column:wy_id"`
Name string `gorm:"column:name"`
}
if err := h.DB.WithContext(ctx).
Model(&models.Competition{}).
Select("wy_id", "name").
Where("wy_id IN ?", competitionWyIDs).
Find(&comps).Error; err != nil {
return nil, err
}
for _, c := range comps {
competitionByWyID[c.WyID] = c.Name
}
}
seasonByWyID := map[int]string{}
if len(seasonWyIDs) > 0 {
var seasons []struct {
WyID int `gorm:"column:wy_id"`
Name string `gorm:"column:name"`
}
if err := h.DB.WithContext(ctx).
Model(&models.Season{}).
Select("wy_id", "name").
Where("wy_id IN ?", seasonWyIDs).
Find(&seasons).Error; err != nil {
return nil, err
}
for _, s := range seasons {
seasonByWyID[s.WyID] = s.Name
}
}
roundByWyID := map[int]string{}
if len(roundWyIDs) > 0 {
var rounds []struct {
WyID int `gorm:"column:wy_id"`
Name string `gorm:"column:name"`
}
if err := h.DB.WithContext(ctx).
Model(&models.Round{}).
Select("wy_id", "name").
Where("wy_id IN ?", roundWyIDs).
Find(&rounds).Error; err != nil {
return nil, err
}
for _, r := range rounds {
roundByWyID[r.WyID] = r.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
}
}
}
teamByWyID := map[int]struct {
ID string
Name string
Image *string
}{}
if len(teamWyIDs) > 0 {
var teams []struct {
WyID int `gorm:"column:wy_id"`
ID string `gorm:"column:id"`
Name string `gorm:"column:name"`
ImageDataURL *string `gorm:"column:image_data_url"`
}
if err := h.DB.WithContext(ctx).
Model(&models.Team{}).
Select("wy_id", "id", "name", "image_data_url").
Where("wy_id IN ?", teamWyIDs).
Find(&teams).Error; err != nil {
return nil, err
}
for _, t := range teams {
teamByWyID[t.WyID] = struct {
ID string
Name string
Image *string
}{ID: t.ID, Name: t.Name, Image: t.ImageDataURL}
}
}
out := make([]matchOut, 0, len(matches))
for _, m := range matches {
if m.ID == "" && m.TsID != nil && *m.TsID != "" {
if id, ok := matchIDByTsID[*m.TsID]; ok {
m.ID = id
}
}
var homeTeam *matchTeamOut
if m.HomeTeamWyID != nil {
if t, ok := teamByWyID[*m.HomeTeamWyID]; ok {
wyID := *m.HomeTeamWyID
homeTeam = &matchTeamOut{WyID: &wyID}
if t.ID != "" {
id := t.ID
homeTeam.ID = &id
}
if t.Name != "" {
name := t.Name
homeTeam.Name = &name
}
if t.Image != nil && *t.Image != "" {
img := *t.Image
homeTeam.Image = &img
}
} else {
wyID := *m.HomeTeamWyID
homeTeam = &matchTeamOut{WyID: &wyID}
}
}
var awayTeam *matchTeamOut
if m.AwayTeamWyID != nil {
if t, ok := teamByWyID[*m.AwayTeamWyID]; ok {
wyID := *m.AwayTeamWyID
awayTeam = &matchTeamOut{WyID: &wyID}
if t.ID != "" {
id := t.ID
awayTeam.ID = &id
}
if t.Name != "" {
name := t.Name
awayTeam.Name = &name
}
if t.Image != nil && *t.Image != "" {
img := *t.Image
awayTeam.Image = &img
}
} else {
wyID := *m.AwayTeamWyID
awayTeam = &matchTeamOut{WyID: &wyID}
}
}
var comp *matchEntityOut
if m.CompetitionWyID != nil {
wyID := *m.CompetitionWyID
comp = &matchEntityOut{WyID: &wyID}
if name, ok := competitionByWyID[*m.CompetitionWyID]; ok && name != "" {
n := name
comp.Name = &n
}
}
var season *matchEntityOut
if m.SeasonWyID != nil {
wyID := *m.SeasonWyID
season = &matchEntityOut{WyID: &wyID}
if name, ok := seasonByWyID[*m.SeasonWyID]; ok && name != "" {
n := name
season.Name = &n
}
}
var round *matchEntityOut
if m.RoundWyID != nil {
wyID := *m.RoundWyID
round = &matchEntityOut{WyID: &wyID}
if name, ok := roundByWyID[*m.RoundWyID]; ok && name != "" {
n := name
round.Name = &n
}
}
out = append(out, matchOut{
Match: m,
HomeTeam: homeTeam,
AwayTeam: awayTeam,
Competition: comp,
Season: season,
Round: round,
})
}
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
}
// GetByMatchWyID
// @Summary Get match by Wyscout match wy_id
// @Description Returns a match by its Wyscout wy_id.
// @Tags Matches
// @Param wyId path int true "Wyscout wy_id identifier"
// @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/wyscout/{wyId} [get]
func (h *MatchHandler) GetByMatchWyID(c *gin.Context) {
wyIDStr := c.Param("wyId")
wyID, err := strconv.Atoi(wyIDStr)
if err != nil || wyID <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid wyId"})
return
}
match, err := h.Service.GetByWyID(c.Request.Context(), wyID)
if err != nil {
respondError(c, err)
return
}
matchWithNames, err := h.enrichMatch(c.Request.Context(), match)
if err != nil {
respondError(c, err)
return
}
endpoint := fmt.Sprintf("/matches/wyscout/%d", wyID)
c.JSON(http.StatusOK, MatchResponse{
Data: matchWithNames,
Meta: map[string]interface{}{
"timestamp": time.Now().UTC().Format(time.RFC3339),
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetLineupByMatchWyID
// @Summary Get match lineup by Wyscout match wy_id
// @Description Returns the stored lineup data for a match (teams/formations/coaches + players) by match wy_id.
// @Tags Matches
// @Param wyId path int true "Wyscout wy_id identifier"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /matches/wyscout/{wyId}/lineup [get]
func (h *MatchHandler) GetLineupByMatchWyID(c *gin.Context) {
wyIDStr := c.Param("wyId")
wyID, err := strconv.Atoi(wyIDStr)
if err != nil || wyID <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid wyId"})
return
}
match, teams, lineup, err := h.Service.GetLineupByWyID(c.Request.Context(), wyID)
if err != nil {
respondError(c, err)
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 != "" {
teamTsIDToSide[*t.TeamTsID] = t.Side
}
}
type lineupPlayerOut struct {
models.MatchLineupPlayer
Side string `json:"side"`
}
playersBySide := map[string][]lineupPlayerOut{
"home": {},
"away": {},
"unknown": {},
}
for _, p := range lineup {
side := "unknown"
if p.TeamTsID != nil {
if s, ok := teamTsIDToSide[*p.TeamTsID]; ok && s != "" {
side = s
}
}
playersBySide[side] = append(playersBySide[side], lineupPlayerOut{MatchLineupPlayer: p, Side: side})
}
teamsBySide := map[string]*models.MatchTeam{
"home": nil,
"away": nil,
}
for _, t := range teams {
teamCopy := t
if teamCopy.Side == "home" {
teamsBySide["home"] = &teamCopy
} else if teamCopy.Side == "away" {
teamsBySide["away"] = &teamCopy
}
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/matches/wyscout/%d/lineup", wyID)
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"match": matchWithNames,
"teams": gin.H{
"home": teamsBySide["home"],
"away": teamsBySide["away"],
},
"players": gin.H{
"home": playersBySide["home"],
"away": playersBySide["away"],
"unknown": playersBySide["unknown"],
},
},
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// List matches
// @Summary List matches
// @Description Returns a paginated list of matches, optionally filtered by competitionWyId, seasonWyId, teamTsId/teamWyId (+teamSide) and date range.
// @Tags Matches
// @Param limit query int false "Maximum number of items to return (default 100)"
// @Param offset query int false "Number of items to skip before starting to collect the result set (default 0)"
// @Param competitionWyId query int false "Filter by competition wy_id"
// @Param seasonWyId query int false "Filter by season wy_id"
// @Param teamTsId query string false "Filter by team ts_id (matches where team is home or away)"
// @Param teamWyId query int false "Filter by team wy_id (matches where team is home or away)"
// @Param teamSide query string false "When filtering by teamTsId/teamWyId, restrict side: home|away|either (default either)"
// @Param from query string false "Filter matches on/after date (RFC3339 or YYYY-MM-DD)"
// @Param to query string false "Filter matches on/before date (RFC3339 or YYYY-MM-DD)"
// @Param status query string false "Filter by match status"
// @Param order query string false "Sort order by match_date: asc|desc (default desc)"
// @Success 200 {object} handlers.MatchListResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /matches [get]
func (h *MatchHandler) 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
}
var competitionWyID *int
if v := c.Query("competitionWyId"); v != "" {
iv, err := strconv.Atoi(v)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid competitionWyId"})
return
}
competitionWyID = &iv
}
var seasonWyID *int
if v := c.Query("seasonWyId"); v != "" {
iv, err := strconv.Atoi(v)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid seasonWyId"})
return
}
seasonWyID = &iv
}
var teamTsID *string
if v := c.Query("teamTsId"); v != "" {
teamTsID = &v
}
var teamWyID *int
if v := c.Query("teamWyId"); v != "" {
iv, err := strconv.Atoi(v)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid teamWyId"})
return
}
teamWyID = &iv
}
if teamTsID != nil && teamWyID != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "use either teamTsId or teamWyId, not both"})
return
}
var teamSide *string
if v := c.Query("teamSide"); v != "" {
teamSide = &v
}
parseDate := func(raw string) (*time.Time, error) {
if raw == "" {
return nil, nil
}
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
}
var status *string
if v := c.Query("status"); v != "" {
status = &v
}
order := c.DefaultQuery("order", "desc")
if order != "asc" && order != "desc" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid order (use asc or desc)"})
return
}
opts := services.ListMatchesOptions{
Limit: limit,
Offset: offset,
CompetitionWyID: competitionWyID,
SeasonWyID: seasonWyID,
TeamTsID: teamTsID,
TeamWyID: teamWyID,
TeamSide: teamSide,
FromDate: from,
ToDate: to,
Status: status,
Order: order,
}
matches, total, err := h.Service.ListMatches(c.Request.Context(), opts)
if err != nil {
respondError(c, err)
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)
endpoint := "/matches"
c.JSON(http.StatusOK, gin.H{
"data": matchesWithNames,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
"totalItems": total,
"page": page,
"limit": limit,
"hasMore": hasMore,
},
})
}
// HeadToHeadMostRecent
// @Summary List matches between two teams
// @Description Returns a paginated list of matches where teamHomeWyId played at home and teamAwayWyId played away (by wy_id).
// @Tags Matches
// @Param teamHomeWyId query int true "Home team wy_id"
// @Param teamAwayWyId query int true "Away team wy_id"
// @Param limit query int false "Maximum number of items to return (default 100)"
// @Param offset query int false "Number of items to skip before starting to collect the result set (default 0)"
// @Param from query string false "Filter matches on/after date (RFC3339 or YYYY-MM-DD)"
// @Param to query string false "Filter matches on/before date (RFC3339 or YYYY-MM-DD)"
// @Success 200 {object} handlers.MatchListResponse
// @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) {
teamHomeStr := c.Query("teamHomeWyId")
teamAwayStr := c.Query("teamAwayWyId")
limitStr := c.DefaultQuery("limit", "100")
offsetStr := c.DefaultQuery("offset", "0")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
limit = 100
}
offset, err := strconv.Atoi(offsetStr)
if err != nil || offset < 0 {
offset = 0
}
parseDate := func(raw string) (*time.Time, error) {
if raw == "" {
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
}
teamHomeWyID, err := strconv.Atoi(teamHomeStr)
if err != nil || teamHomeWyID <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid teamHomeWyId"})
return
}
teamAwayWyID, err := strconv.Atoi(teamAwayStr)
if err != nil || teamAwayWyID <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid teamAwayWyId"})
return
}
matches, total, err := h.Service.ListHeadToHead(c.Request.Context(), teamHomeWyID, teamAwayWyID, from, to, limit, offset)
if err != nil {
respondError(c, err)
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)
endpoint := fmt.Sprintf("/matches/head-to-head?teamHomeWyId=%d&teamAwayWyId=%d&limit=%d&offset=%d", teamHomeWyID, teamAwayWyID, limit, offset)
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": matchesWithNames,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
"totalItems": total,
"page": page,
"limit": limit,
"hasMore": hasMore,
},
})
}
// GetLineupByMatchTsID
// @Summary Get match lineup by match id
// @Description Returns the stored lineup data for a match (teams/formations/coaches + players) by match ts_id.
// @Tags Matches
// @Param matchTsId path string true "TheSports match id (tsId)"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /matches/{matchTsId}/lineup [get]
func (h *MatchHandler) GetLineupByMatchTsID(c *gin.Context) {
matchTsID := c.Param("matchTsId")
if matchTsID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "matchTsId is required"})
return
}
match, teams, lineup, err := h.Service.GetLineup(c.Request.Context(), matchTsID)
if err != nil {
respondError(c, err)
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 != "" {
teamTsIDToSide[*t.TeamTsID] = t.Side
}
}
type lineupPlayerOut struct {
models.MatchLineupPlayer
Side string `json:"side"`
}
playersBySide := map[string][]lineupPlayerOut{
"home": {},
"away": {},
"unknown": {},
}
for _, p := range lineup {
side := "unknown"
if p.TeamTsID != nil {
if s, ok := teamTsIDToSide[*p.TeamTsID]; ok && s != "" {
side = s
}
}
playersBySide[side] = append(playersBySide[side], lineupPlayerOut{MatchLineupPlayer: p, Side: side})
}
teamsBySide := map[string]*models.MatchTeam{
"home": nil,
"away": nil,
}
for _, t := range teams {
teamCopy := t
if teamCopy.Side == "home" {
teamsBySide["home"] = &teamCopy
} else if teamCopy.Side == "away" {
teamsBySide["away"] = &teamCopy
}
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/matches/%s/lineup", matchTsID)
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"match": matchWithNames,
"teams": gin.H{
"home": teamsBySide["home"],
"away": teamsBySide["away"],
},
"players": gin.H{
"home": playersBySide["home"],
"away": playersBySide["away"],
"unknown": playersBySide["unknown"],
},
},
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
package handlers
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/models"
"ScoutingSystemScoreData/internal/services"
)
type PlayerHandler struct {
Service services.PlayerService
Teams services.TeamService
Areas services.AreaService
}
func RegisterPlayerRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewPlayerService(db)
teamService := services.NewTeamService(db)
areaService := services.NewAreaService(db)
h := &PlayerHandler{Service: service, Teams: teamService, Areas: areaService}
players := rg.Group("/players")
players.GET("", h.List)
players.GET("/wyscout/:wyId", h.GetByProviderID)
players.GET("/provider/:providerId", h.GetByAnyProviderID)
players.GET("/:id", h.GetByID)
}
type PlayerRole struct {
Name string `json:"name"`
Code2 string `json:"code2"`
Code3 string `json:"code3"`
}
// Meta represents metadata for paginated responses.
type Meta struct {
Timestamp string `json:"timestamp"`
Endpoint string `json:"endpoint"`
Method string `json:"method"`
TotalItems int64 `json:"totalItems,omitempty"`
Page int `json:"page,omitempty"`
Limit int `json:"limit,omitempty"`
HasMore bool `json:"hasMore,omitempty"`
}
type StructuredPlayer struct {
ID string `json:"id"`
TsID string `json:"tsId"`
WyID int `json:"wyId"`
GSMID *int `json:"-"`
ShortName string `json:"shortName"`
FirstName string `json:"firstName"`
MiddleName *string `json:"-"`
LastName string `json:"lastName"`
Height *int `json:"height"`
Weight *float64 `json:"weight"`
BirthDate *time.Time `json:"birthDate"`
Role PlayerRole `json:"role"`
Position *string `json:"position"`
Foot *string `json:"foot"`
CurrentTeamID *int `json:"-"`
CurrentNationalTeamID *int `json:"-"`
CountryTsID *string `json:"-"`
CountryName *string `json:"-"`
Gender *string `json:"gender"`
Status string `json:"status"`
JerseyNumber *int `json:"-"`
ImageDataURL *string `json:"imageDataURL"`
APILastSyncedAt *time.Time `json:"apiLastSyncedAt"`
APISyncStatus string `json:"apiSyncStatus"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
MarketValue *int `json:"-"`
MarketValueCurrency *string `json:"-"`
ContractUntil *time.Time `json:"-"`
AbilityJSON *string `json:"-"`
CharacteristicsJSON *string `json:"-"`
UID *string `json:"-"`
Deathday *time.Time `json:"-"`
RetireTime *time.Time `json:"-"`
BirthArea *AreaSummary `json:"birthArea"`
PassportArea *AreaSummary `json:"passportArea"`
Team *TeamSummary `json:"-"`
CurrentTeam *TeamDTO `json:"currentTeam"`
CurrentNationalTeam *TeamDTO `json:"currentNationalTeam"`
}
func toAreaSummary(a models.Area) AreaSummary {
return AreaSummary{
ID: a.ID,
WyID: a.WyID,
TsID: strOrEmpty(a.TsID),
Alpha2Code: strOrEmpty(a.Alpha2Code),
Alpha3Code: strOrEmpty(a.Alpha3Code),
Name: a.Name,
}
}
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"`
AreaWyID *int `json:"areaWyId"`
AreaName *string `json:"areaName"`
Area *AreaDTO `json:"area"`
}
type AreaSummary struct {
ID string `json:"id"`
WyID string `json:"wyId"`
TsID string `json:"tsId"`
Alpha2Code string `json:"alpha2code"`
Alpha3Code string `json:"alpha3code"`
Name string `json:"name"`
}
func strOrEmpty(s *string) string {
if s == nil {
return ""
}
return *s
}
func toStructuredPlayer(p models.Player) StructuredPlayer {
wyID := 0
if p.WyID != nil {
wyID = *p.WyID
}
role := PlayerRole{
Name: "",
Code2: "",
Code3: "",
}
if p.RoleName != nil {
role.Name = *p.RoleName
}
if p.RoleCode2 != nil {
role.Code2 = *p.RoleCode2
}
if p.RoleCode3 != nil {
role.Code3 = *p.RoleCode3
}
status := p.Status
if status == "" {
if p.IsActive {
status = "active"
} else {
status = "inactive"
}
}
return StructuredPlayer{
ID: p.ID,
TsID: strOrEmpty(p.TsID),
WyID: wyID,
GSMID: p.GSMID,
ShortName: valueOrDefault(p.ShortName, ""),
FirstName: p.FirstName,
MiddleName: p.MiddleName,
LastName: p.LastName,
Height: p.HeightCM,
Weight: p.WeightKG,
BirthDate: p.DateOfBirth,
Role: role,
Position: p.Position,
Foot: p.Foot,
CurrentTeamID: p.CurrentTeamID,
CurrentNationalTeamID: p.CurrentNationalTeamID,
CountryTsID: p.CountryTsID,
CountryName: nil,
Gender: p.Gender,
Status: status,
JerseyNumber: p.JerseyNumber,
ImageDataURL: p.ImageDataURL,
APILastSyncedAt: p.APILastSyncedAt,
APISyncStatus: p.APISyncStatus,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
MarketValue: p.MarketValue,
MarketValueCurrency: p.MarketValueCurrency,
ContractUntil: p.ContractUntil,
AbilityJSON: p.AbilityJSON,
CharacteristicsJSON: p.CharacteristicsJSON,
UID: p.UID,
Deathday: p.Deathday,
RetireTime: p.RetireTime,
}
}
func toTeamSummary(t models.Team) TeamSummary {
return TeamSummary{
ID: t.ID,
TsID: strOrEmpty(t.TsID),
WyID: t.WyID,
Name: t.Name,
ShortName: t.ShortName,
ImageDataURL: t.ImageDataURL,
AreaWyID: t.AreaWyID,
AreaName: nil,
Area: nil,
}
}
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 addPlayerTeamsByWyID(structured []StructuredPlayer, players []models.Player, teamsByWyID map[int]models.Team) {
if len(structured) != len(players) {
return
}
for i := range structured {
if players[i].TeamWyID == nil {
continue
}
id := *players[i].TeamWyID
if id <= 0 {
continue
}
if t, ok := teamsByWyID[id]; 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 addPlayerTeamAreas(structured []StructuredPlayer, areasByWyID map[int]models.Area) {
for i := range structured {
if structured[i].Team != nil && structured[i].Team.AreaWyID != nil {
if a, ok := areasByWyID[*structured[i].Team.AreaWyID]; ok {
name := a.Name
structured[i].Team.AreaName = &name
dto := toAreaDTO(a)
structured[i].Team.Area = &dto
}
}
}
}
func addPlayerCurrentTeams(structured []StructuredPlayer, players []models.Player, teamsByWyID map[int]models.Team, areasByWyID map[int]models.Area) {
if len(structured) != len(players) {
return
}
for i := range structured {
if players[i].CurrentTeamID != nil {
if t, ok := teamsByWyID[*players[i].CurrentTeamID]; ok {
var areaName *string
var areaDTO *AreaDTO
if t.AreaWyID != nil {
if a, ok := areasByWyID[*t.AreaWyID]; ok {
name := a.Name
areaName = &name
d := toAreaDTO(a)
areaDTO = &d
}
}
dto := toTeamDTO(t, areaName, areaDTO)
structured[i].CurrentTeam = &dto
}
}
if players[i].CurrentNationalTeamID != nil {
if t, ok := teamsByWyID[*players[i].CurrentNationalTeamID]; ok {
var areaName *string
var areaDTO *AreaDTO
if t.AreaWyID != nil {
if a, ok := areasByWyID[*t.AreaWyID]; ok {
name := a.Name
areaName = &name
d := toAreaDTO(a)
areaDTO = &d
}
}
dto := toTeamDTO(t, areaName, areaDTO)
structured[i].CurrentNationalTeam = &dto
}
}
}
}
func valueOrDefault(s *string, def string) string {
if s != nil {
return *s
}
return def
}
// List players
// @Summary List players
// @Description Returns a paginated list of players, optionally filtered by name, team, or country.
// @Tags Players
// @Param limit query int false "Maximum number of items to return (default 100)"
// @Param offset query int false "Number of items to skip before starting to collect the result set (default 0)"
// @Param name query string false "Filter players by name (short, first, middle, or last)"
// @Param teamId query string false "Filter players by current team ID"
// @Param country query string false "Filter players by birth country name"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Router /players [get]
func (h *PlayerHandler) 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
}
name := c.Query("name")
teamID := c.Query("teamId")
country := c.Query("country")
endpoint := "/players"
if name != "" {
endpoint = fmt.Sprintf("/players?name=%s", name)
} else if teamID != "" {
endpoint = fmt.Sprintf("/players?teamId=%s", teamID)
} else if country != "" {
endpoint = fmt.Sprintf("/players?country=%s", country)
}
players, total, err := h.Service.ListPlayers(c.Request.Context(), services.ListPlayersOptions{
Limit: limit,
Offset: offset,
Name: name,
TeamID: teamID,
Country: country,
})
if err != nil {
respondError(c, err)
return
}
structured := make([]StructuredPlayer, 0, len(players))
for _, p := range players {
structured = append(structured, toStructuredPlayer(p))
}
wyIDs := make([]int, 0, len(players))
wySeen := make(map[int]struct{}, len(players))
for _, p := range players {
if p.CurrentTeamID != nil {
id := *p.CurrentTeamID
if id > 0 {
if _, ok := wySeen[id]; !ok {
wySeen[id] = struct{}{}
wyIDs = append(wyIDs, id)
}
}
}
if p.CurrentNationalTeamID != nil {
id := *p.CurrentNationalTeamID
if id > 0 {
if _, ok := wySeen[id]; !ok {
wySeen[id] = struct{}{}
wyIDs = append(wyIDs, id)
}
}
}
}
areasByWyID := make(map[int]models.Area)
if len(wyIDs) > 0 {
teams, err := h.Teams.GetByWyIDs(c.Request.Context(), wyIDs)
if err != nil {
respondError(c, err)
return
}
teamsByWyID := make(map[int]models.Team, len(teams))
areaWyIDs := make([]int, 0, len(teams))
areaSeen := make(map[int]struct{}, len(teams))
for _, t := range teams {
if t.WyID == nil {
continue
}
teamsByWyID[*t.WyID] = t
if t.AreaWyID != nil && *t.AreaWyID > 0 {
if _, ok := areaSeen[*t.AreaWyID]; !ok {
areaSeen[*t.AreaWyID] = struct{}{}
areaWyIDs = append(areaWyIDs, *t.AreaWyID)
}
}
}
if len(areaWyIDs) > 0 {
areas, err := h.Areas.GetByWyIDs(c.Request.Context(), areaWyIDs)
if err != nil {
respondError(c, err)
return
}
for _, a := range areas {
wyStr := strings.TrimSpace(a.WyID)
if wyStr == "" {
continue
}
wy, err := strconv.Atoi(wyStr)
if err != nil {
continue
}
areasByWyID[wy] = a
}
}
addPlayerCurrentTeams(structured, players, teamsByWyID, areasByWyID)
addPlayerTeamAreas(structured, areasByWyID)
}
teamWyIDs := make([]int, 0, len(players))
teamWySeen := make(map[int]struct{}, len(players))
for _, p := range players {
if p.TeamWyID == nil {
continue
}
id := *p.TeamWyID
if id <= 0 {
continue
}
if _, ok := teamWySeen[id]; ok {
continue
}
teamWySeen[id] = struct{}{}
teamWyIDs = append(teamWyIDs, id)
}
if len(teamWyIDs) > 0 {
teams, err := h.Teams.GetByWyIDs(c.Request.Context(), teamWyIDs)
if err != nil {
respondError(c, err)
return
}
teamsByWyID := make(map[int]models.Team, len(teams))
areaWyIDs := make([]int, 0, len(teams))
areaSeen := make(map[int]struct{}, len(teams))
for _, t := range teams {
if t.WyID == nil {
continue
}
teamsByWyID[*t.WyID] = t
if t.AreaWyID != nil && *t.AreaWyID > 0 {
if _, ok := areaSeen[*t.AreaWyID]; !ok {
areaSeen[*t.AreaWyID] = struct{}{}
areaWyIDs = append(areaWyIDs, *t.AreaWyID)
}
}
}
if len(areaWyIDs) > 0 {
areas, err := h.Areas.GetByWyIDs(c.Request.Context(), areaWyIDs)
if err != nil {
respondError(c, err)
return
}
for _, a := range areas {
wyStr := strings.TrimSpace(a.WyID)
if wyStr == "" {
continue
}
wy, err := strconv.Atoi(wyStr)
if err != nil {
continue
}
areasByWyID[wy] = a
}
}
addPlayerTeamsByWyID(structured, players, teamsByWyID)
addPlayerTeamAreas(structured, areasByWyID)
}
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 {
if a.TsID == nil || strings.TrimSpace(*a.TsID) == "" {
continue
}
areasByTsID[strings.TrimSpace(*a.TsID)] = a
}
addPlayerCountries(structured, players, areasByTsID)
}
page := offset/limit + 1
hasMore := int64(offset+limit) < total
timestamp := time.Now().UTC().Format(time.RFC3339)
c.JSON(http.StatusOK, gin.H{
"data": structured,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
"totalItems": total,
"page": page,
"limit": limit,
"hasMore": hasMore,
},
})
}
// GetByID returns a single player by internal ID
// @Summary Get player by ID
// @Description Returns a single player by its internal ID.
// @Tags Players
// @Param id path string true "Player internal identifier"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /players/{id} [get]
func (h *PlayerHandler) GetByID(c *gin.Context) {
id := c.Param("id")
player, err := h.Service.GetByID(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
structured := toStructuredPlayer(player)
if player.CurrentTeamID != nil || player.CurrentNationalTeamID != nil {
wyIDs := make([]int, 0, 2)
wySeen := make(map[int]struct{}, 2)
if player.CurrentTeamID != nil && *player.CurrentTeamID > 0 {
wySeen[*player.CurrentTeamID] = struct{}{}
wyIDs = append(wyIDs, *player.CurrentTeamID)
}
if player.CurrentNationalTeamID != nil && *player.CurrentNationalTeamID > 0 {
if _, ok := wySeen[*player.CurrentNationalTeamID]; !ok {
wyIDs = append(wyIDs, *player.CurrentNationalTeamID)
}
}
if len(wyIDs) > 0 {
teams, err := h.Teams.GetByWyIDs(c.Request.Context(), wyIDs)
if err != nil {
respondError(c, err)
return
}
teamsByWyID := make(map[int]models.Team, len(teams))
areaWyIDs := make([]int, 0, len(teams))
areaSeen := make(map[int]struct{}, len(teams))
for _, t := range teams {
if t.WyID == nil {
continue
}
teamsByWyID[*t.WyID] = t
if t.AreaWyID != nil && *t.AreaWyID > 0 {
if _, ok := areaSeen[*t.AreaWyID]; !ok {
areaSeen[*t.AreaWyID] = struct{}{}
areaWyIDs = append(areaWyIDs, *t.AreaWyID)
}
}
}
areasByWyID := make(map[int]models.Area)
if len(areaWyIDs) > 0 {
areas, err := h.Areas.GetByWyIDs(c.Request.Context(), areaWyIDs)
if err != nil {
respondError(c, err)
return
}
for _, a := range areas {
wyStr := strings.TrimSpace(a.WyID)
if wyStr == "" {
continue
}
wy, err := strconv.Atoi(wyStr)
if err != nil {
continue
}
areasByWyID[wy] = a
}
}
tmp := []StructuredPlayer{structured}
addPlayerCurrentTeams(tmp, []models.Player{player}, teamsByWyID, areasByWyID)
addPlayerTeamAreas(tmp, areasByWyID)
structured = tmp[0]
}
}
// Enrich birthArea/passportArea by wy_id if available
wyIDs := make([]int, 0, 2)
if player.BirthAreaWyID != nil && *player.BirthAreaWyID > 0 {
wyIDs = append(wyIDs, *player.BirthAreaWyID)
}
if player.PassportAreaWyID != nil && *player.PassportAreaWyID > 0 {
wyIDs = append(wyIDs, *player.PassportAreaWyID)
}
if len(wyIDs) > 0 {
areas, err := h.Areas.GetByWyIDs(c.Request.Context(), wyIDs)
if err != nil {
respondError(c, err)
return
}
byWy := map[string]models.Area{}
for _, a := range areas {
if strings.TrimSpace(a.WyID) != "" {
byWy[strings.TrimSpace(a.WyID)] = a
}
}
if player.BirthAreaWyID != nil {
if a, ok := byWy[strconv.Itoa(*player.BirthAreaWyID)]; ok {
s := toAreaSummary(a)
structured.BirthArea = &s
}
}
if player.PassportAreaWyID != nil {
if a, ok := byWy[strconv.Itoa(*player.PassportAreaWyID)]; ok {
s := toAreaSummary(a)
structured.PassportArea = &s
}
}
}
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 {
// team enrichment uses WyScout IDs only
}
if player.TeamWyID != nil && *player.TeamWyID > 0 {
teams, err := h.Teams.GetByWyIDs(c.Request.Context(), []int{*player.TeamWyID})
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)
c.JSON(http.StatusOK, gin.H{
"data": structured,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByAnyProviderID returns a single player by wy_id (numeric) or ts_id (string)
func (h *PlayerHandler) GetByAnyProviderID(c *gin.Context) {
providerID := c.Param("providerId")
player, err := h.Service.GetByAnyProviderID(c.Request.Context(), providerID)
if err != nil {
respondError(c, err)
return
}
structured := toStructuredPlayer(player)
if player.CurrentTeamID != nil || player.CurrentNationalTeamID != nil {
wyIDs := make([]int, 0, 2)
wySeen := make(map[int]struct{}, 2)
if player.CurrentTeamID != nil && *player.CurrentTeamID > 0 {
wySeen[*player.CurrentTeamID] = struct{}{}
wyIDs = append(wyIDs, *player.CurrentTeamID)
}
if player.CurrentNationalTeamID != nil && *player.CurrentNationalTeamID > 0 {
if _, ok := wySeen[*player.CurrentNationalTeamID]; !ok {
wyIDs = append(wyIDs, *player.CurrentNationalTeamID)
}
}
if len(wyIDs) > 0 {
teams, err := h.Teams.GetByWyIDs(c.Request.Context(), wyIDs)
if err != nil {
respondError(c, err)
return
}
teamsByWyID := make(map[int]models.Team, len(teams))
areaWyIDs := make([]int, 0, len(teams))
areaSeen := make(map[int]struct{}, len(teams))
for _, t := range teams {
if t.WyID == nil {
continue
}
teamsByWyID[*t.WyID] = t
if t.AreaWyID != nil && *t.AreaWyID > 0 {
if _, ok := areaSeen[*t.AreaWyID]; !ok {
areaSeen[*t.AreaWyID] = struct{}{}
areaWyIDs = append(areaWyIDs, *t.AreaWyID)
}
}
}
areasByWyID := make(map[int]models.Area)
if len(areaWyIDs) > 0 {
areas, err := h.Areas.GetByWyIDs(c.Request.Context(), areaWyIDs)
if err != nil {
respondError(c, err)
return
}
for _, a := range areas {
wyStr := strings.TrimSpace(a.WyID)
if wyStr == "" {
continue
}
wy, err := strconv.Atoi(wyStr)
if err != nil {
continue
}
areasByWyID[wy] = a
}
}
tmp := []StructuredPlayer{structured}
addPlayerCurrentTeams(tmp, []models.Player{player}, teamsByWyID, areasByWyID)
structured = tmp[0]
}
}
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.TeamWyID != nil && *player.TeamWyID > 0 {
teams, err := h.Teams.GetByWyIDs(c.Request.Context(), []int{*player.TeamWyID})
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)
c.JSON(http.StatusOK, gin.H{
"data": structured,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByProviderID returns a single player by WyScout provider ID
// @Summary Get player by provider ID
// @Description Returns a single player by its provider (wy_id) identifier.
// @Tags Players
// @Param wyId path string true "Provider identifier (wy_id numeric or ts_id string)"
// @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 /players/wyscout/{wyId} [get]
func (h *PlayerHandler) GetByProviderID(c *gin.Context) {
wyIDStr := c.Param("wyId")
wyID, err := strconv.Atoi(wyIDStr)
var player models.Player
if err == nil {
player, err = h.Service.GetByProviderID(c.Request.Context(), wyID)
} else {
player, err = h.Service.GetByAnyProviderID(c.Request.Context(), wyIDStr)
}
if err != nil {
respondError(c, err)
return
}
structured := toStructuredPlayer(player)
if player.CurrentTeamID != nil || player.CurrentNationalTeamID != nil {
wyIDs := make([]int, 0, 2)
wySeen := make(map[int]struct{}, 2)
if player.CurrentTeamID != nil && *player.CurrentTeamID > 0 {
wySeen[*player.CurrentTeamID] = struct{}{}
wyIDs = append(wyIDs, *player.CurrentTeamID)
}
if player.CurrentNationalTeamID != nil && *player.CurrentNationalTeamID > 0 {
if _, ok := wySeen[*player.CurrentNationalTeamID]; !ok {
wyIDs = append(wyIDs, *player.CurrentNationalTeamID)
}
}
if len(wyIDs) > 0 {
teams, err := h.Teams.GetByWyIDs(c.Request.Context(), wyIDs)
if err != nil {
respondError(c, err)
return
}
teamsByWyID := make(map[int]models.Team, len(teams))
areaWyIDs := make([]int, 0, len(teams))
areaSeen := make(map[int]struct{}, len(teams))
for _, t := range teams {
if t.WyID == nil {
continue
}
teamsByWyID[*t.WyID] = t
if t.AreaWyID != nil && *t.AreaWyID > 0 {
if _, ok := areaSeen[*t.AreaWyID]; !ok {
areaSeen[*t.AreaWyID] = struct{}{}
areaWyIDs = append(areaWyIDs, *t.AreaWyID)
}
}
}
areasByWyID := make(map[int]models.Area)
if len(areaWyIDs) > 0 {
areas, err := h.Areas.GetByWyIDs(c.Request.Context(), areaWyIDs)
if err != nil {
respondError(c, err)
return
}
for _, a := range areas {
wyStr := strings.TrimSpace(a.WyID)
if wyStr == "" {
continue
}
wy, err := strconv.Atoi(wyStr)
if err != nil {
continue
}
areasByWyID[wy] = a
}
}
tmp := []StructuredPlayer{structured}
addPlayerCurrentTeams(tmp, []models.Player{player}, teamsByWyID, areasByWyID)
structured = tmp[0]
}
}
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.TeamWyID != nil && *player.TeamWyID > 0 {
teams, err := h.Teams.GetByWyIDs(c.Request.Context(), []int{*player.TeamWyID})
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/%s", wyIDStr)
c.JSON(http.StatusOK, gin.H{
"data": structured,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
package handlers
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/models"
"ScoutingSystemScoreData/internal/services"
)
type RefereeHandler struct {
Service services.RefereeService
}
func RegisterRefereeRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewRefereeService(db)
h := &RefereeHandler{Service: service}
referees := rg.Group("/referees")
referees.GET("", h.List)
referees.GET("/wyscout/:wyId", h.GetByWyID)
referees.GET("/provider/:providerId", h.GetByProviderID)
referees.GET(":id", h.GetByID)
}
type StructuredReferee struct {
ID string `json:"id"`
WyID *int `json:"wyId"`
TsID string `json:"tsId"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
MiddleName *string `json:"middleName"`
ShortName *string `json:"shortName"`
DateOfBirth *time.Time `json:"dateOfBirth"`
NationalityWyID *int `json:"nationalityWyId"`
RefereeType string `json:"refereeType"`
FIFACategory *string `json:"fifaCategory"`
ExperienceYears *int `json:"experienceYears"`
UID *string `json:"uid"`
Status string `json:"status"`
ImageDataURL *string `json:"imageDataUrl"`
APILastSyncedAt *time.Time `json:"apiLastSyncedAt"`
APISyncStatus string `json:"apiSyncStatus"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt"`
}
func toStructuredReferee(r models.Referee) StructuredReferee {
tsID := ""
if r.TsID != nil {
tsID = *r.TsID
}
return StructuredReferee{
ID: r.ID,
WyID: nilIfZero(r.WyID),
TsID: tsID,
FirstName: r.FirstName,
LastName: r.LastName,
MiddleName: r.MiddleName,
ShortName: r.ShortName,
DateOfBirth: r.DateOfBirth,
NationalityWyID: r.NationalityWyID,
RefereeType: r.RefereeType,
FIFACategory: r.FIFACategory,
ExperienceYears: r.ExperienceYears,
UID: r.UID,
Status: r.Status,
ImageDataURL: r.ImageDataURL,
APILastSyncedAt: r.APILastSyncedAt,
APISyncStatus: r.APISyncStatus,
IsActive: r.IsActive,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
DeletedAt: r.DeletedAt,
}
}
// List referees
// @Summary List referees
// @Description Returns a paginated list of referees, optionally filtered by name, referee type, nationality, or active status.
// @Tags Referees
// @Param limit query int false "Maximum number of items to return (default 100)"
// @Param offset query int false "Number of items to skip before starting to collect the result set (default 0)"
// @Param name query string false "Filter referees by name (first, last, middle, or short)"
// @Param countryWyId query int false "Filter referees by nationality WyID"
// @Param type query string false "Filter referees by type (main, assistant, var, etc.)"
// @Param active query bool false "Filter active referees only"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Router /referees [get]
func (h *RefereeHandler) 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
}
name := c.Query("name")
countryWyIDStr := c.Query("countryWyId")
refType := c.Query("type")
activeStr := c.Query("active")
activeOnly := activeStr == "true" || activeStr == "1"
var countryWyID *int
if countryWyIDStr != "" {
if v, err := strconv.Atoi(countryWyIDStr); err == nil {
countryWyID = &v
}
}
endpoint := "/referees"
if name != "" {
endpoint = fmt.Sprintf("/referees?name=%s", name)
} else if countryWyIDStr != "" {
endpoint = fmt.Sprintf("/referees?countryWyId=%s", countryWyIDStr)
} else if refType != "" {
endpoint = fmt.Sprintf("/referees?type=%s", refType)
} else if activeOnly {
endpoint = "/referees?active=true"
}
referees, total, err := h.Service.ListReferees(c.Request.Context(), services.ListRefereesOptions{
Limit: limit,
Offset: offset,
Name: name,
CountryWyID: countryWyID,
Type: refType,
ActiveOnly: activeOnly,
})
if err != nil {
respondError(c, err)
return
}
structured := make([]StructuredReferee, 0, len(referees))
for _, r := range referees {
structured = append(structured, toStructuredReferee(r))
}
page := offset/limit + 1
hasMore := int64(offset+limit) < total
timestamp := time.Now().UTC().Format(time.RFC3339)
c.JSON(http.StatusOK, gin.H{
"data": structured,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
"totalItems": total,
"page": page,
"limit": limit,
"hasMore": hasMore,
},
})
}
// GetByID returns a single referee by internal ID
// @Summary Get referee by ID
// @Description Returns a single referee by its internal ID.
// @Tags Referees
// @Param id path string true "Referee internal identifier"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /referees/{id} [get]
func (h *RefereeHandler) GetByID(c *gin.Context) {
id := c.Param("id")
referee, err := h.Service.GetByID(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
structured := toStructuredReferee(referee)
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/referees/%s", id)
c.JSON(http.StatusOK, gin.H{
"data": structured,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByProviderID returns a single referee by wy_id (numeric) or ts_id (string)
// @Summary Get referee by provider ID
// @Description Returns a single referee by its provider identifier: numeric values are treated as Wyscout wy_id, non-numeric as TheSports ts_id.
// @Tags Referees
// @Param providerId path string true "Provider identifier (wy_id or ts_id)"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /referees/provider/{providerId} [get]
func (h *RefereeHandler) GetByProviderID(c *gin.Context) {
providerID := c.Param("providerId")
referee, err := h.Service.GetByAnyProviderID(c.Request.Context(), providerID)
if err != nil {
respondError(c, err)
return
}
structured := toStructuredReferee(referee)
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/referees/provider/%s", providerID)
c.JSON(http.StatusOK, gin.H{
"data": structured,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByWyID returns a single referee by WyScout WyID
// @Summary Get referee by Wyscout ID
// @Description Returns a single referee by its Wyscout wy_id identifier.
// @Tags Referees
// @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 /referees/wyscout/{wyId} [get]
func (h *RefereeHandler) 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
}
referee, err := h.Service.GetByWyID(c.Request.Context(), wyID)
if err != nil {
respondError(c, err)
return
}
structured := toStructuredReferee(referee)
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/referees/wyscout/%d", wyID)
c.JSON(http.StatusOK, gin.H{
"data": structured,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
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)"
// @Param name query string false "Filter by season name (case-insensitive, partial match)"
// @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")
nameStr := c.Query("name")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
limit = 100
}
offset, err := strconv.Atoi(offsetStr)
if err != nil || offset < 0 {
offset = 0
}
var name *string
if nameStr != "" {
name = &nameStr
}
seasons, total, err := h.Service.ListSeasons(c.Request.Context(), limit, offset, name)
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",
},
})
}
package handlers
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/models"
)
type StandingHandler struct {
DB *gorm.DB
}
type StandingTeamOut struct {
WyID *int `json:"wyId,omitempty"`
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
Image *string `json:"image,omitempty"`
}
type StandingOut struct {
CompetitionWyID *int `json:"competitionWyId,omitempty"`
SeasonWyID *int `json:"seasonWyId,omitempty"`
TeamWyID *int `json:"teamWyId,omitempty"`
GroupName string `json:"groupName"`
Position *int `json:"position,omitempty"`
TotalPoints int `json:"totalPoints"`
TotalPlayed int `json:"totalPlayed"`
TotalWins int `json:"totalWins"`
TotalDraws int `json:"totalDraws"`
TotalLosses int `json:"totalLosses"`
TotalGoalsFor int `json:"totalGoalsFor"`
TotalGoalsAgainst int `json:"totalGoalsAgainst"`
GoalDifference int `json:"goalDifference"`
PointsPerGame float64 `json:"pointsPerGame"`
APILastSyncedAt *time.Time `json:"apiLastSyncedAt,omitempty"`
APISyncStatus string `json:"apiSyncStatus"`
Team *StandingTeamOut `json:"team,omitempty"`
}
type StandingListResponse struct {
Data []StandingOut `json:"data"`
Meta map[string]interface{} `json:"meta"`
}
func RegisterStandingRoutes(rg *gin.RouterGroup, db *gorm.DB) {
h := &StandingHandler{DB: db}
standings := rg.Group("/standings")
standings.GET("", h.List)
standings.GET("/season/:seasonWyId", h.ListBySeason)
}
// List standings
// @Summary List standings
// @Description Returns a paginated list of standings. Provide seasonWyId or competitionWyId to scope results.
// @Tags Standings
// @Param seasonWyId query int false "Filter by season wy_id"
// @Param competitionWyId query int false "Filter by competition wy_id"
// @Param teamWyId query int false "Filter by team wy_id"
// @Param groupName query string false "Filter by group name (exact match)"
// @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} handlers.StandingListResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /standings [get]
func (h *StandingHandler) 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
}
var seasonWyID *int
if v := strings.TrimSpace(c.Query("seasonWyId")); v != "" {
n, err := strconv.Atoi(v)
if err != nil || n <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid seasonWyId"})
return
}
seasonWyID = &n
}
var competitionWyID *int
if v := strings.TrimSpace(c.Query("competitionWyId")); v != "" {
n, err := strconv.Atoi(v)
if err != nil || n <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid competitionWyId"})
return
}
competitionWyID = &n
}
var teamWyID *int
if v := strings.TrimSpace(c.Query("teamWyId")); v != "" {
n, err := strconv.Atoi(v)
if err != nil || n <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid teamWyId"})
return
}
teamWyID = &n
}
groupName := strings.TrimSpace(c.Query("groupName"))
if seasonWyID == nil && competitionWyID == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "provide seasonWyId or competitionWyId"})
return
}
q := h.DB.WithContext(c.Request.Context()).Model(&models.Standing{})
if seasonWyID != nil {
q = q.Where("season_wy_id = ?", *seasonWyID)
}
if competitionWyID != nil {
q = q.Where("competition_wy_id = ?", *competitionWyID)
}
if teamWyID != nil {
q = q.Where("team_wy_id = ?", *teamWyID)
}
if groupName != "" {
q = q.Where("group_name = ?", groupName)
}
var total int64
if err := q.Count(&total).Error; err != nil {
respondError(c, err)
return
}
var rows []models.Standing
if err := q.
Order("group_name asc").
Order("position asc").
Limit(limit).
Offset(offset).
Find(&rows).Error; err != nil {
respondError(c, err)
return
}
teamWyIDs := make([]int, 0, len(rows))
seenTeams := make(map[int]struct{}, len(rows))
for _, r := range rows {
if r.TeamWyID == nil || *r.TeamWyID <= 0 {
continue
}
id := *r.TeamWyID
if _, ok := seenTeams[id]; ok {
continue
}
seenTeams[id] = struct{}{}
teamWyIDs = append(teamWyIDs, id)
}
teamByWyID := make(map[int]struct {
ID string
Name string
Image *string
}, len(teamWyIDs))
if len(teamWyIDs) > 0 {
var teams []struct {
WyID int `gorm:"column:wy_id"`
ID string `gorm:"column:id"`
Name string `gorm:"column:name"`
ImageDataURL *string `gorm:"column:image_data_url"`
}
if err := h.DB.WithContext(c.Request.Context()).
Model(&models.Team{}).
Select("wy_id", "id", "name", "image_data_url").
Where("wy_id IN ?", teamWyIDs).
Find(&teams).Error; err != nil {
respondError(c, err)
return
}
for _, t := range teams {
teamByWyID[t.WyID] = struct {
ID string
Name string
Image *string
}{ID: t.ID, Name: t.Name, Image: t.ImageDataURL}
}
}
out := make([]StandingOut, 0, len(rows))
for _, r := range rows {
item := StandingOut{
CompetitionWyID: r.CompetitionWyID,
SeasonWyID: r.SeasonWyID,
TeamWyID: r.TeamWyID,
GroupName: r.GroupName,
Position: r.Position,
TotalPoints: r.TotalPoints,
TotalPlayed: r.TotalPlayed,
TotalWins: r.TotalWins,
TotalDraws: r.TotalDraws,
TotalLosses: r.TotalLosses,
TotalGoalsFor: r.TotalGoalsFor,
TotalGoalsAgainst: r.TotalGoalsAgainst,
GoalDifference: r.GoalDifference,
PointsPerGame: r.PointsPerGame,
APILastSyncedAt: r.APILastSyncedAt,
APISyncStatus: r.APISyncStatus,
}
if r.TeamWyID != nil {
wyID := *r.TeamWyID
team := &StandingTeamOut{WyID: &wyID}
if t, ok := teamByWyID[wyID]; ok {
if t.ID != "" {
id := t.ID
team.ID = &id
}
if strings.TrimSpace(t.Name) != "" {
name := t.Name
team.Name = &name
}
if t.Image != nil && strings.TrimSpace(*t.Image) != "" {
img := *t.Image
team.Image = &img
}
}
item.Team = team
}
out = append(out, item)
}
page := offset/limit + 1
hasMore := int64(offset+limit) < total
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := "/standings"
c.JSON(http.StatusOK, gin.H{
"data": out,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
"totalItems": total,
"page": page,
"limit": limit,
"hasMore": hasMore,
},
})
}
// List standings by season
// @Summary List standings by season
// @Description Convenience endpoint to list standings for a single season.
// @Tags Standings
// @Param seasonWyId path int true "Wyscout season wy_id"
// @Param competitionWyId query int false "Optional competition wy_id filter"
// @Param teamWyId query int false "Optional team wy_id filter"
// @Param groupName query string false "Filter by group name (exact match)"
// @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} handlers.StandingListResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /standings/season/{seasonWyId} [get]
func (h *StandingHandler) ListBySeason(c *gin.Context) {
seasonStr := c.Param("seasonWyId")
seasonWyID, err := strconv.Atoi(seasonStr)
if err != nil || seasonWyID <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid seasonWyId"})
return
}
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
}
var competitionWyID *int
if v := strings.TrimSpace(c.Query("competitionWyId")); v != "" {
n, err := strconv.Atoi(v)
if err != nil || n <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid competitionWyId"})
return
}
competitionWyID = &n
}
var teamWyID *int
if v := strings.TrimSpace(c.Query("teamWyId")); v != "" {
n, err := strconv.Atoi(v)
if err != nil || n <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid teamWyId"})
return
}
teamWyID = &n
}
groupName := strings.TrimSpace(c.Query("groupName"))
q := h.DB.WithContext(c.Request.Context()).Model(&models.Standing{}).
Where("season_wy_id = ?", seasonWyID)
if competitionWyID != nil {
q = q.Where("competition_wy_id = ?", *competitionWyID)
}
if teamWyID != nil {
q = q.Where("team_wy_id = ?", *teamWyID)
}
if groupName != "" {
q = q.Where("group_name = ?", groupName)
}
var total int64
if err := q.Count(&total).Error; err != nil {
respondError(c, err)
return
}
var rows []models.Standing
if err := q.
Order("group_name asc").
Order("position asc").
Limit(limit).
Offset(offset).
Find(&rows).Error; err != nil {
respondError(c, err)
return
}
teamWyIDs := make([]int, 0, len(rows))
seenTeams := make(map[int]struct{}, len(rows))
for _, r := range rows {
if r.TeamWyID == nil || *r.TeamWyID <= 0 {
continue
}
id := *r.TeamWyID
if _, ok := seenTeams[id]; ok {
continue
}
seenTeams[id] = struct{}{}
teamWyIDs = append(teamWyIDs, id)
}
teamByWyID := make(map[int]struct {
ID string
Name string
Image *string
}, len(teamWyIDs))
if len(teamWyIDs) > 0 {
var teams []struct {
WyID int `gorm:"column:wy_id"`
ID string `gorm:"column:id"`
Name string `gorm:"column:name"`
ImageDataURL *string `gorm:"column:image_data_url"`
}
if err := h.DB.WithContext(c.Request.Context()).
Model(&models.Team{}).
Select("wy_id", "id", "name", "image_data_url").
Where("wy_id IN ?", teamWyIDs).
Find(&teams).Error; err != nil {
respondError(c, err)
return
}
for _, t := range teams {
teamByWyID[t.WyID] = struct {
ID string
Name string
Image *string
}{ID: t.ID, Name: t.Name, Image: t.ImageDataURL}
}
}
out := make([]StandingOut, 0, len(rows))
for _, r := range rows {
item := StandingOut{
CompetitionWyID: r.CompetitionWyID,
SeasonWyID: r.SeasonWyID,
TeamWyID: r.TeamWyID,
GroupName: r.GroupName,
Position: r.Position,
TotalPoints: r.TotalPoints,
TotalPlayed: r.TotalPlayed,
TotalWins: r.TotalWins,
TotalDraws: r.TotalDraws,
TotalLosses: r.TotalLosses,
TotalGoalsFor: r.TotalGoalsFor,
TotalGoalsAgainst: r.TotalGoalsAgainst,
GoalDifference: r.GoalDifference,
PointsPerGame: r.PointsPerGame,
APILastSyncedAt: r.APILastSyncedAt,
APISyncStatus: r.APISyncStatus,
}
if r.TeamWyID != nil {
wyID := *r.TeamWyID
team := &StandingTeamOut{WyID: &wyID}
if t, ok := teamByWyID[wyID]; ok {
if t.ID != "" {
id := t.ID
team.ID = &id
}
if strings.TrimSpace(t.Name) != "" {
name := t.Name
team.Name = &name
}
if t.Image != nil && strings.TrimSpace(*t.Image) != "" {
img := *t.Image
team.Image = &img
}
}
item.Team = team
}
out = append(out, item)
}
page := offset/limit + 1
hasMore := int64(offset+limit) < total
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/standings/season/%d", seasonWyID)
if raw := c.Request.URL.RawQuery; raw != "" {
endpoint = fmt.Sprintf("%s?%s", endpoint, raw)
}
c.JSON(http.StatusOK, gin.H{
"data": out,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
"totalItems": total,
"page": page,
"limit": limit,
"hasMore": hasMore,
},
})
}
package handlers
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/models"
"ScoutingSystemScoreData/internal/services"
)
type TeamListResponse struct {
Data []TeamDTO `json:"data"`
Meta map[string]interface{} `json:"meta"`
}
type TeamResponse struct {
Data TeamDTO `json:"data"`
Meta map[string]interface{} `json:"meta"`
}
type TeamDTO struct {
ID string `json:"id"`
WyID *int `json:"wyId"`
Name string `json:"name"`
OfficialName *string `json:"officialName"`
Type string `json:"type"`
Category string `json:"category"`
Gender *string `json:"gender"`
AreaWyID *int `json:"areaWyId"`
AreaName *string `json:"areaName"`
Area *AreaDTO `json:"area"`
City *string `json:"city"`
ImageDataURL *string `json:"imageDataUrl"`
Status string `json:"status"`
IsActive bool `json:"isActive"`
APILastSyncedAt *time.Time `json:"apiLastSyncedAt"`
APISyncStatus string `json:"apiSyncStatus"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt"`
}
type AreaDTO struct {
ID int `json:"id"`
Alpha2Code *string `json:"alpha2code"`
Alpha3Code *string `json:"alpha3code"`
Name string `json:"name"`
}
func toAreaDTO(a models.Area) AreaDTO {
wy, _ := strconv.Atoi(strings.TrimSpace(a.WyID))
return AreaDTO{
ID: wy,
Alpha2Code: a.Alpha2Code,
Alpha3Code: a.Alpha3Code,
Name: a.Name,
}
}
func toTeamDTO(t models.Team, areaName *string, area *AreaDTO) TeamDTO {
return TeamDTO{
ID: t.ID,
WyID: t.WyID,
Name: t.Name,
OfficialName: t.OfficialName,
Type: t.Type,
Category: t.Category,
Gender: t.Gender,
AreaWyID: t.AreaWyID,
AreaName: areaName,
Area: area,
City: t.City,
ImageDataURL: t.ImageDataURL,
Status: t.Status,
IsActive: t.IsActive,
APILastSyncedAt: t.APILastSyncedAt,
APISyncStatus: t.APISyncStatus,
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
DeletedAt: t.DeletedAt,
}
}
type TeamHandler struct {
DB *gorm.DB
Service services.TeamService
Players services.PlayerService
Areas services.AreaService
}
func RegisterTeamRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewTeamService(db)
playerService := services.NewPlayerService(db)
areaService := services.NewAreaService(db)
h := &TeamHandler{DB: db, Service: service, Players: playerService, Areas: areaService}
teams := rg.Group("/teams")
teams.GET("", h.List)
teams.GET("/wyscout/:wyId", h.GetByWyID)
teams.GET("/wyscout/:wyId/players", h.ListPlayersByWyID)
teams.GET("/wyscout/:wyId/images", h.GetImagesByWyID)
teams.GET("/provider/:providerId", h.GetByProviderID)
teams.GET("/:id/images", h.GetImagesByID)
teams.GET("/:id", h.GetByID)
}
// ListPlayersByWyID returns all players for a team given its Wyscout wy_id.
// @Summary List players by team Wyscout ID
// @Description Returns players for a given team wy_id.
// @Tags Teams
// @Param wyId path int true "Wyscout wy_id identifier"
// @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 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /teams/wyscout/{wyId}/players [get]
func (h *TeamHandler) ListPlayersByWyID(c *gin.Context) {
wyIDStr := c.Param("wyId")
wyID, err := strconv.Atoi(wyIDStr)
if err != nil || wyID <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid wyId"})
return
}
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
}
teamID := strconv.Itoa(wyID)
endpoint := fmt.Sprintf("/teams/wyscout/%d/players", wyID)
players, total, err := h.Players.ListPlayers(c.Request.Context(), services.ListPlayersOptions{
Limit: limit,
Offset: offset,
TeamID: teamID,
})
if err != nil {
respondError(c, err)
return
}
structured := make([]StructuredPlayer, 0, len(players))
for _, p := range players {
structured = append(structured, toStructuredPlayer(p))
}
wyIDs := make([]int, 0, len(players))
wySeen := make(map[int]struct{}, len(players))
for _, p := range players {
if p.CurrentTeamID != nil {
cid := *p.CurrentTeamID
if cid > 0 {
if _, ok := wySeen[cid]; !ok {
wySeen[cid] = struct{}{}
wyIDs = append(wyIDs, cid)
}
}
}
if p.CurrentNationalTeamID != nil {
nid := *p.CurrentNationalTeamID
if nid > 0 {
if _, ok := wySeen[nid]; !ok {
wySeen[nid] = struct{}{}
wyIDs = append(wyIDs, nid)
}
}
}
}
areasByWyID := make(map[int]models.Area)
if len(wyIDs) > 0 {
teams, err := h.Service.GetByWyIDs(c.Request.Context(), wyIDs)
if err != nil {
respondError(c, err)
return
}
teamsByWyID := make(map[int]models.Team, len(teams))
areaWyIDs := make([]int, 0, len(teams))
areaSeen := make(map[int]struct{}, len(teams))
for _, t := range teams {
if t.WyID == nil {
continue
}
teamsByWyID[*t.WyID] = t
if t.AreaWyID != nil && *t.AreaWyID > 0 {
if _, ok := areaSeen[*t.AreaWyID]; !ok {
areaSeen[*t.AreaWyID] = struct{}{}
areaWyIDs = append(areaWyIDs, *t.AreaWyID)
}
}
}
if len(areaWyIDs) > 0 {
areas, err := h.Areas.GetByWyIDs(c.Request.Context(), areaWyIDs)
if err != nil {
respondError(c, err)
return
}
for _, a := range areas {
wyStr := strings.TrimSpace(a.WyID)
if wyStr == "" {
continue
}
wy, err := strconv.Atoi(wyStr)
if err != nil {
continue
}
areasByWyID[wy] = a
}
}
addPlayerCurrentTeams(structured, players, teamsByWyID, areasByWyID)
addPlayerTeamAreas(structured, areasByWyID)
}
teamWyIDs := make([]int, 0, len(players))
teamWySeen := make(map[int]struct{}, len(players))
for _, p := range players {
if p.TeamWyID == nil {
continue
}
tid := *p.TeamWyID
if tid <= 0 {
continue
}
if _, ok := teamWySeen[tid]; ok {
continue
}
teamWySeen[tid] = struct{}{}
teamWyIDs = append(teamWyIDs, tid)
}
if len(teamWyIDs) > 0 {
teams, err := h.Service.GetByWyIDs(c.Request.Context(), teamWyIDs)
if err != nil {
respondError(c, err)
return
}
teamsByWyID := make(map[int]models.Team, len(teams))
areaWyIDs := make([]int, 0, len(teams))
areaSeen := make(map[int]struct{}, len(teams))
for _, t := range teams {
if t.WyID == nil {
continue
}
teamsByWyID[*t.WyID] = t
if t.AreaWyID != nil && *t.AreaWyID > 0 {
if _, ok := areaSeen[*t.AreaWyID]; !ok {
areaSeen[*t.AreaWyID] = struct{}{}
areaWyIDs = append(areaWyIDs, *t.AreaWyID)
}
}
}
if len(areaWyIDs) > 0 {
areas, err := h.Areas.GetByWyIDs(c.Request.Context(), areaWyIDs)
if err != nil {
respondError(c, err)
return
}
for _, a := range areas {
wyStr := strings.TrimSpace(a.WyID)
if wyStr == "" {
continue
}
wy, err := strconv.Atoi(wyStr)
if err != nil {
continue
}
areasByWyID[wy] = a
}
}
addPlayerTeamsByWyID(structured, players, teamsByWyID)
addPlayerTeamAreas(structured, areasByWyID)
}
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 {
if a.TsID == nil || strings.TrimSpace(*a.TsID) == "" {
continue
}
areasByTsID[strings.TrimSpace(*a.TsID)] = a
}
addPlayerCountries(structured, players, areasByTsID)
}
page := offset/limit + 1
hasMore := int64(offset+limit) < total
timestamp := time.Now().UTC().Format(time.RFC3339)
c.JSON(http.StatusOK, gin.H{
"data": structured,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
"totalItems": total,
"page": page,
"limit": limit,
"hasMore": hasMore,
},
})
}
type TeamImagesResponse struct {
Data struct {
WyID int `json:"wyId"`
ImageDataURL *string `json:"imageDataUrl"`
Children []struct {
WyID int `json:"wyId"`
Name *string `json:"name"`
ImageDataURL *string `json:"imageDataUrl"`
} `json:"children"`
} `json:"data"`
Meta map[string]interface{} `json:"meta"`
}
// GetImagesByWyID returns stored imageDataUrl for a team and its children.
// @Summary Get team images by Wyscout ID
// @Description Returns imageDataUrl for a given team wyId and its child teams.
// @Tags Teams
// @Param wyId path int true "Wyscout wy_id identifier"
// @Success 200 {object} handlers.TeamImagesResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /teams/wyscout/{wyId}/images [get]
func (h *TeamHandler) GetImagesByWyID(c *gin.Context) {
wyIDStr := c.Param("wyId")
wyID, err := strconv.Atoi(wyIDStr)
if err != nil || wyID <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid wyId"})
return
}
team, err := h.Service.GetByWyID(c.Request.Context(), wyID)
if err != nil {
respondError(c, err)
return
}
var rels []models.TeamChild
if err := h.DB.Where("parent_team_wy_id = ?", wyID).Find(&rels).Error; err != nil {
respondError(c, err)
return
}
childIDs := make([]int, 0, len(rels))
nameByWyID := make(map[int]*string, len(rels))
for _, r := range rels {
if r.ChildTeamWyID <= 0 {
continue
}
childIDs = append(childIDs, r.ChildTeamWyID)
nameByWyID[r.ChildTeamWyID] = r.ChildTeamName
}
children := make(map[int]models.Team)
if len(childIDs) > 0 {
rows, err := h.Service.GetByWyIDs(c.Request.Context(), childIDs)
if err != nil {
respondError(c, err)
return
}
for _, t := range rows {
if t.WyID == nil {
continue
}
children[*t.WyID] = t
}
}
var resp TeamImagesResponse
resp.Data.WyID = wyID
resp.Data.ImageDataURL = team.ImageDataURL
resp.Data.Children = make([]struct {
WyID int `json:"wyId"`
Name *string `json:"name"`
ImageDataURL *string `json:"imageDataUrl"`
}, 0, len(childIDs))
for _, id := range childIDs {
item := struct {
WyID int `json:"wyId"`
Name *string `json:"name"`
ImageDataURL *string `json:"imageDataUrl"`
}{WyID: id, Name: nameByWyID[id]}
if t, ok := children[id]; ok {
item.ImageDataURL = t.ImageDataURL
if item.Name == nil {
if strings.TrimSpace(t.Name) != "" {
name := t.Name
item.Name = &name
}
}
}
resp.Data.Children = append(resp.Data.Children, item)
}
timestamp := time.Now().UTC().Format(time.RFC3339)
resp.Meta = gin.H{
"timestamp": timestamp,
"endpoint": fmt.Sprintf("/teams/wyscout/%d/images", wyID),
"method": "GET",
}
c.JSON(http.StatusOK, resp)
}
// GetImagesByID returns stored imageDataUrl for a team and its children by internal DB id.
// @Summary Get team images by internal ID
// @Description Returns imageDataUrl for a given team id and its child teams.
// @Tags Teams
// @Param id path string true "Team internal identifier"
// @Success 200 {object} handlers.TeamImagesResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /teams/{id}/images [get]
func (h *TeamHandler) GetImagesByID(c *gin.Context) {
id := c.Param("id")
team, err := h.Service.GetByID(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
if team.WyID == nil || *team.WyID <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "team missing wyId"})
return
}
wyID := *team.WyID
var rels []models.TeamChild
if err := h.DB.Where("parent_team_wy_id = ?", wyID).Find(&rels).Error; err != nil {
respondError(c, err)
return
}
childIDs := make([]int, 0, len(rels))
nameByWyID := make(map[int]*string, len(rels))
for _, r := range rels {
if r.ChildTeamWyID <= 0 {
continue
}
childIDs = append(childIDs, r.ChildTeamWyID)
nameByWyID[r.ChildTeamWyID] = r.ChildTeamName
}
children := make(map[int]models.Team)
if len(childIDs) > 0 {
rows, err := h.Service.GetByWyIDs(c.Request.Context(), childIDs)
if err != nil {
respondError(c, err)
return
}
for _, t := range rows {
if t.WyID == nil {
continue
}
children[*t.WyID] = t
}
}
var resp TeamImagesResponse
resp.Data.WyID = wyID
resp.Data.ImageDataURL = team.ImageDataURL
resp.Data.Children = make([]struct {
WyID int `json:"wyId"`
Name *string `json:"name"`
ImageDataURL *string `json:"imageDataUrl"`
}, 0, len(childIDs))
for _, cid := range childIDs {
item := struct {
WyID int `json:"wyId"`
Name *string `json:"name"`
ImageDataURL *string `json:"imageDataUrl"`
}{WyID: cid, Name: nameByWyID[cid]}
if t, ok := children[cid]; ok {
item.ImageDataURL = t.ImageDataURL
if item.Name == nil {
if strings.TrimSpace(t.Name) != "" {
name := t.Name
item.Name = &name
}
}
}
resp.Data.Children = append(resp.Data.Children, item)
}
timestamp := time.Now().UTC().Format(time.RFC3339)
resp.Meta = gin.H{
"timestamp": timestamp,
"endpoint": fmt.Sprintf("/teams/%s/images", id),
"method": "GET",
}
c.JSON(http.StatusOK, resp)
}
// List teams
// @Summary List teams
// @Description Returns a list of teams with optional pagination.
// @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} handlers.TeamListResponse
// @Failure 500 {object} map[string]string
// @Router /teams [get]
func (h *TeamHandler) 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
}
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
}
areaWyIDs := make([]int, 0, len(teams))
areaSeen := make(map[int]struct{}, len(teams))
for _, t := range teams {
if t.AreaWyID == nil {
continue
}
id := *t.AreaWyID
if id <= 0 {
continue
}
if _, ok := areaSeen[id]; ok {
continue
}
areaSeen[id] = struct{}{}
areaWyIDs = append(areaWyIDs, id)
}
areasByWyID := make(map[int]models.Area)
if len(areaWyIDs) > 0 {
areas, err := h.Areas.GetByWyIDs(c.Request.Context(), areaWyIDs)
if err != nil {
respondError(c, err)
return
}
areasByWyID = make(map[int]models.Area, len(areas))
for _, a := range areas {
wyStr := strings.TrimSpace(a.WyID)
if wyStr == "" {
continue
}
wy, err := strconv.Atoi(wyStr)
if err != nil {
continue
}
areasByWyID[wy] = a
}
}
withArea := make([]TeamDTO, 0, len(teams))
for _, t := range teams {
var areaName *string
var areaDTO *AreaDTO
if t.AreaWyID != nil {
if a, ok := areasByWyID[*t.AreaWyID]; ok {
name := a.Name
areaName = &name
dto := toAreaDTO(a)
areaDTO = &dto
}
}
withArea = append(withArea, toTeamDTO(t, areaName, areaDTO))
}
page := offset/limit + 1
hasMore := int64(offset+limit) < total
timestamp := time.Now().UTC().Format(time.RFC3339)
c.JSON(http.StatusOK, gin.H{
"data": withArea,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
"totalItems": total,
"page": page,
"limit": limit,
"hasMore": hasMore,
},
})
}
// GetByID returns a single team by internal ID
// @Summary Get team by ID
// @Description Returns a single team by its internal ID.
// @Tags Teams
// @Param id path string true "Team internal identifier"
// @Success 200 {object} handlers.TeamResponse
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /teams/{id} [get]
func (h *TeamHandler) GetByID(c *gin.Context) {
id := c.Param("id")
team, err := h.Service.GetByID(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
var areaName *string
var areaDTO *AreaDTO
if team.AreaWyID != nil && *team.AreaWyID > 0 {
areas, err := h.Areas.GetByWyIDs(c.Request.Context(), []int{*team.AreaWyID})
if err != nil {
respondError(c, err)
return
}
if len(areas) > 0 {
name := areas[0].Name
areaName = &name
dto := toAreaDTO(areas[0])
areaDTO = &dto
}
}
data := toTeamDTO(team, areaName, areaDTO)
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/teams/%s", id)
c.JSON(http.StatusOK, gin.H{
"data": data,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByProviderID returns a single team by wy_id (numeric) or ts_id (string)
func (h *TeamHandler) GetByProviderID(c *gin.Context) {
providerID := c.Param("providerId")
team, err := h.Service.GetByAnyProviderID(c.Request.Context(), providerID)
if err != nil {
respondError(c, err)
return
}
var areaName *string
var areaDTO *AreaDTO
if team.AreaWyID != nil && *team.AreaWyID > 0 {
areas, err := h.Areas.GetByWyIDs(c.Request.Context(), []int{*team.AreaWyID})
if err != nil {
respondError(c, err)
return
}
if len(areas) > 0 {
name := areas[0].Name
areaName = &name
d := toAreaDTO(areas[0])
areaDTO = &d
}
}
data := toTeamDTO(team, areaName, areaDTO)
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/teams/provider/%s", providerID)
c.JSON(http.StatusOK, gin.H{
"data": data,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByWyID returns a single team by WyScout WyID
// @Summary Get team by Wyscout ID
// @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} handlers.TeamResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /teams/wyscout/{wyId} [get]
func (h *TeamHandler) 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
}
team, err := h.Service.GetByWyID(c.Request.Context(), wyID)
if err != nil {
respondError(c, err)
return
}
var areaName *string
var areaDTO *AreaDTO
if team.AreaWyID != nil && *team.AreaWyID > 0 {
areas, err := h.Areas.GetByWyIDs(c.Request.Context(), []int{*team.AreaWyID})
if err != nil {
respondError(c, err)
return
}
if len(areas) > 0 {
name := areas[0].Name
areaName = &name
d := toAreaDTO(areas[0])
areaDTO = &d
}
}
data := toTeamDTO(team, areaName, areaDTO)
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/teams/wyscout/%d", wyID)
c.JSON(http.StatusOK, gin.H{
"data": data,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
package models
import (
"encoding/json"
"time"
gonanoid "github.com/matoous/go-nanoid/v2"
"gorm.io/gorm"
)
// Area mirrors the Nest/Drizzle `areas` table.
type Area struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
WyID string `gorm:"column:wy_id" json:"wyId"`
TsID *string `gorm:"column:ts_id" json:"tsId"`
Name string `gorm:"column:name" json:"name"`
Alpha2Code *string `gorm:"column:alpha2code" json:"alpha2code"`
Alpha3Code *string `gorm:"column:alpha3code" json:"alpha3code"`
CategoryID *string `gorm:"column:ts_category_id" json:"categoryId"`
LogoURL *string `gorm:"column:logo" json:"logo"`
TheSportsUpdatedAt *time.Time `gorm:"column:ts_updated_at" json:"theSportsUpdatedAt"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at" json:"deletedAt"`
}
func (a *Area) BeforeCreate(tx *gorm.DB) (err error) {
if a.ID != "" {
return nil
}
id, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 15)
if err != nil {
return err
}
a.ID = id
return nil
}
// Competition mirrors the Nest/Drizzle `competitions` table.
type Competition struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
WyID *int `gorm:"column:wy_id" json:"wyId"`
TsID *string `gorm:"column:ts_id;size:64;uniqueIndex" json:"tsId"`
TsCategoryID *string `gorm:"column:ts_category_id;size:64" json:"tsCategoryId"`
CountryTsID *string `gorm:"column:country_ts_id;size:64" json:"countryTsId"`
Name string `gorm:"column:name" json:"name"`
ShortName *string `gorm:"column:short_name" json:"shortName"`
Logo *string `gorm:"column:logo" json:"logo"`
OfficialName *string `gorm:"column:official_name" json:"officialName"`
AreaWyID *int `gorm:"column:area_wy_id" json:"areaWyId"`
Gender *string `gorm:"column:gender;type:gender" json:"gender"`
TsType *int `gorm:"column:ts_type" json:"tsType"`
CurSeasonTsID *string `gorm:"column:cur_season_ts_id;size:64" json:"curSeasonTsId"`
CurStageTsID *string `gorm:"column:cur_stage_ts_id;size:64" json:"curStageTsId"`
CurRound *int `gorm:"column:cur_round" json:"curRound"`
RoundCount *int `gorm:"column:round_count" json:"roundCount"`
TitleHolderJSON json.RawMessage `gorm:"column:title_holder_json;type:jsonb" json:"titleHolder" swaggertype:"array,object"`
MostTitlesJSON json.RawMessage `gorm:"column:most_titles_json;type:jsonb" json:"mostTitles" swaggertype:"array,object"`
NewcomersJSON json.RawMessage `gorm:"column:newcomers_json;type:jsonb" json:"newcomers" swaggertype:"array,object"`
DivisionsJSON json.RawMessage `gorm:"column:divisions_json;type:jsonb" json:"divisions" swaggertype:"array,object"`
HostJSON json.RawMessage `gorm:"column:host_json;type:jsonb" json:"host" swaggertype:"object"`
UID *string `gorm:"column:uid" json:"uid"`
TheSportsUpdatedAt *time.Time `gorm:"column:ts_updated_at" json:"theSportsUpdatedAt"`
Type string `gorm:"column:type;type:competition_type;default:league" json:"type"`
Category string `gorm:"column:category;default:default" json:"category"`
IsActive bool `gorm:"column:is_active;default:true" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at" json:"deletedAt"`
}
func (c *Competition) BeforeCreate(tx *gorm.DB) (err error) {
if c.ID != "" {
return nil
}
id, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 15)
if err != nil {
return err
}
c.ID = id
return nil
}
// Season mirrors the Nest/Drizzle `seasons` table.
type Season struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
WyID *int `gorm:"column:wy_id" json:"wyId"`
TsID *string `gorm:"column:ts_id;size:64;uniqueIndex" json:"tsId"`
CompetitionWyID *int `gorm:"column:competition_wy_id" json:"competitionWyId"`
Name string `gorm:"column:name" json:"name"`
StartDate *time.Time `gorm:"column:start_date" json:"startDate"`
EndDate *time.Time `gorm:"column:end_date" json:"endDate"`
IsActive bool `gorm:"column:is_active;default:true" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at" json:"deletedAt"`
}
type TeamChild struct {
ParentTeamWyID int `gorm:"primaryKey;column:parent_team_wy_id" json:"parentTeamWyId"`
ChildTeamWyID int `gorm:"primaryKey;column:child_team_wy_id" json:"childTeamWyId"`
ChildTeamName *string `gorm:"column:child_team_name" json:"childTeamName"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at" json:"deletedAt"`
}
func (s *Season) BeforeCreate(tx *gorm.DB) (err error) {
if s.ID != "" {
return nil
}
id, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 15)
if err != nil {
return err
}
s.ID = id
return nil
}
type Team 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"`
GSMID *int `gorm:"column:gsm_id" json:"gsmId"`
Name string `gorm:"column:name" json:"name"`
OfficialName *string `gorm:"column:official_name" json:"officialName"`
ShortName *string `gorm:"column:short_name" json:"shortName"`
Description *string `gorm:"column:description" json:"description"`
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"`
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"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at" json:"deletedAt"`
}
func (t *Team) BeforeCreate(tx *gorm.DB) (err error) {
if t.ID != "" {
return nil
}
id, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 15)
if err != nil {
return err
}
t.ID = id
return nil
}
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"`
ShortName *string `gorm:"column:short_name" json:"shortName"`
DateOfBirth *time.Time `gorm:"column:date_of_birth" json:"dateOfBirth"`
NationalityWyID *int `gorm:"column:nationality_wy_id" json:"nationalityWyId"`
CurrentTeamWyID *int `gorm:"column:current_team_wy_id" json:"currentTeamWyId"`
Position string `gorm:"column:position;type:coach_position;default:head_coach" json:"position"`
CoachingLicense *string `gorm:"column:coaching_license" json:"coachingLicense"`
YearsExperience *int `gorm:"column:years_experience" json:"yearsExperience"`
PreferredFormation *string `gorm:"column:preferred_formation" json:"preferredFormation"`
JoinedAt *time.Time `gorm:"column:joined_at" json:"joinedAt"`
ContractUntil *time.Time `gorm:"column:contract_until" json:"contractUntil"`
UID *string `gorm:"column:uid" json:"uid"`
Deathday *time.Time `gorm:"column:deathday" json:"deathday"`
Status string `gorm:"column:status;type:status;default:active" json:"status"`
ImageDataURL *string `gorm:"column:image_data_url" json:"imageDataUrl"`
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"`
IsActive bool `gorm:"column:is_active;default:true" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at" json:"deletedAt"`
}
func (c *Coach) BeforeCreate(tx *gorm.DB) (err error) {
if c.ID != "" {
return nil
}
id, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 15)
if err != nil {
return err
}
c.ID = id
return nil
}
type Referee 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"`
FirstName string `gorm:"column:first_name" json:"firstName"`
LastName string `gorm:"column:last_name" json:"lastName"`
MiddleName *string `gorm:"column:middle_name" json:"middleName"`
ShortName *string `gorm:"column:short_name" json:"shortName"`
DateOfBirth *time.Time `gorm:"column:date_of_birth" json:"dateOfBirth"`
NationalityWyID *int `gorm:"column:nationality_wy_id" json:"nationalityWyId"`
RefereeType string `gorm:"column:referee_type;type:referee_type;default:main" json:"refereeType"`
FIFACategory *string `gorm:"column:fifa_category;type:fifa_category" json:"fifaCategory"`
ExperienceYears *int `gorm:"column:experience_years" json:"experienceYears"`
UID *string `gorm:"column:uid" json:"uid"`
Status string `gorm:"column:status;type:status;default:active" json:"status"`
ImageDataURL *string `gorm:"column:image_data_url" json:"imageDataUrl"`
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"`
IsActive bool `gorm:"column:is_active;default:true" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at" json:"deletedAt"`
}
func (r *Referee) BeforeCreate(tx *gorm.DB) (err error) {
if r.ID != "" {
return nil
}
id, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 15)
if err != nil {
return err
}
r.ID = id
return nil
}
type Player 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"`
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"`
ShortName *string `gorm:"column:short_name" json:"shortName"`
GSMID *int `gorm:"column:gsm_id" json:"gsmId"`
CurrentTeamID *int `gorm:"column:current_team_id" json:"currentTeamId"`
CurrentNationalTeamID *int `gorm:"column:current_national_team_id" json:"currentNationalTeamId"`
DateOfBirth *time.Time `gorm:"column:date_of_birth" json:"dateOfBirth"`
HeightCM *int `gorm:"column:height_cm" json:"heightCm"`
WeightKG *float64 `gorm:"column:weight_kg" json:"weightKg"`
Foot *string `gorm:"column:foot;type:foot" json:"foot"`
Gender *string `gorm:"column:gender;type:gender" json:"gender"`
Position *string `gorm:"column:position" json:"position"`
RoleCode2 *string `gorm:"column:role_code2" json:"roleCode2"`
RoleCode3 *string `gorm:"column:role_code3" json:"roleCode3"`
RoleName *string `gorm:"column:role_name" json:"roleName"`
BirthAreaWyID *int `gorm:"column:birth_area_wy_id" json:"birthAreaWyId"`
PassportAreaWyID *int `gorm:"column:passport_area_wy_id" json:"passportAreaWyId"`
MarketValue *int `gorm:"column:market_value" json:"marketValue"`
MarketValueCurrency *string `gorm:"column:market_value_currency" json:"marketValueCurrency"`
ContractUntil *time.Time `gorm:"column:contract_until" json:"contractUntil"`
AbilityJSON *string `gorm:"column:ability_json;type:jsonb" json:"abilityJson"`
CharacteristicsJSON *string `gorm:"column:characteristics_json;type:jsonb" json:"characteristicsJson"`
UID *string `gorm:"column:uid" json:"uid"`
Deathday *time.Time `gorm:"column:deathday" json:"deathday"`
RetireTime *time.Time `gorm:"column:retire_time" json:"retireTime"`
Status string `gorm:"column:status;type:status;default:active" json:"status"`
ImageDataURL *string `gorm:"column:image_data_url" json:"imageDataUrl"`
JerseyNumber *int `gorm:"column:jersey_number" json:"jerseyNumber"`
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"`
IsActive bool `gorm:"column:is_active;default:true" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at" json:"deletedAt"`
}
func (p *Player) BeforeCreate(tx *gorm.DB) (err error) {
if p.ID != "" {
return nil
}
id, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 15)
if err != nil {
return err
}
p.ID = id
return nil
}
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"`
AwayTeamTsID *string `gorm:"column:away_team_ts_id;size:64" json:"awayTeamTsId"`
CompetitionWyID *int `gorm:"column:competition_wy_id" json:"competitionWyId"`
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"`
MatchType string `gorm:"column:match_type;type:match_type;default:regular" json:"matchType"`
Status string `gorm:"column:status;type:match_status;default:scheduled" json:"status"`
HomeScore int `gorm:"column:home_score;default:0" json:"homeScore"`
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"`
AssistantReferee2WyID *int `gorm:"column:assistant_referee_2_wy_id" json:"assistantReferee2WyId"`
FourthRefereeWyID *int `gorm:"column:fourth_referee_wy_id" json:"fourthRefereeWyId"`
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"`
FixtureDetailsJSON json.RawMessage `gorm:"column:fixture_details_json;type:jsonb" json:"fixtureDetails" swaggertype:"object"`
WyscoutRawJSON json.RawMessage `gorm:"column:wyscout_raw_json;type:jsonb" json:"wyscoutRaw" swaggertype:"object"`
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"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at" json:"deletedAt"`
}
func (m *Match) BeforeCreate(tx *gorm.DB) (err error) {
if m.ID != "" {
return nil
}
id, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 15)
if err != nil {
return err
}
m.ID = id
return nil
}
type MatchTeam struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
MatchTsID string `gorm:"column:match_ts_id;size:64;index" json:"matchTsId"`
MatchWyID *int `gorm:"column:match_wy_id;index" json:"matchWyId"`
TeamWyID *int `gorm:"column:team_wy_id" json:"teamWyId"`
TeamTsID *string `gorm:"column:team_ts_id;size:64" json:"teamTsId"`
Side string `gorm:"column:side" json:"side"`
Formation *string `gorm:"column:formation" json:"formation"`
CoachWyID *int `gorm:"column:coach_wy_id" json:"coachWyId"`
CoachTsID *string `gorm:"column:coach_ts_id;size:64" json:"coachTsId"`
CoverageLineup *bool `gorm:"column:coverage_lineup" json:"coverageLineup"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
}
func (mt *MatchTeam) BeforeCreate(tx *gorm.DB) (err error) {
if mt.ID != "" {
return nil
}
id, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 15)
if err != nil {
return err
}
mt.ID = id
return nil
}
type MatchLineupPlayer struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
MatchTsID string `gorm:"column:match_ts_id;size:64;index" json:"matchTsId"`
MatchWyID *int `gorm:"column:match_wy_id;index" json:"matchWyId"`
TeamWyID *int `gorm:"column:team_wy_id" json:"teamWyId"`
TeamTsID *string `gorm:"column:team_ts_id;size:64" json:"teamTsId"`
PlayerWyID *int `gorm:"column:player_wy_id" json:"playerWyId"`
PlayerTsID *string `gorm:"column:player_ts_id;size:64" json:"playerTsId"`
ShirtNumber *int `gorm:"column:shirt_number" json:"shirtNumber"`
IsStarter bool `gorm:"column:is_starter;default:false" json:"isStarter"`
Position *string `gorm:"column:position" json:"position"`
PosX *float64 `gorm:"column:pos_x" json:"posX"`
PosY *float64 `gorm:"column:pos_y" json:"posY"`
MinuteIn *int `gorm:"column:minute_in" json:"minuteIn"`
MinuteOut *int `gorm:"column:minute_out" json:"minuteOut"`
SubOrder *int `gorm:"column:sub_order" json:"subOrder"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
}
func (ml *MatchLineupPlayer) BeforeCreate(tx *gorm.DB) (err error) {
if ml.ID != "" {
return nil
}
id, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 15)
if err != nil {
return err
}
ml.ID = id
return nil
}
type MatchFormation struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
MatchWyID int `gorm:"column:match_wy_id;index" json:"matchWyId"`
Side string `gorm:"column:side" json:"side"`
WyID *int `gorm:"column:wy_id" json:"wyId"`
Scheme *string `gorm:"column:scheme" json:"scheme"`
StartSec *int `gorm:"column:start_sec" json:"startSec"`
EndSec *int `gorm:"column:end_sec" json:"endSec"`
MatchPeriodStart *string `gorm:"column:match_period_start" json:"matchPeriodStart"`
MatchPeriodEnd *string `gorm:"column:match_period_end" json:"matchPeriodEnd"`
TeamWyID *int `gorm:"column:team_wy_id;index" json:"teamWyId"`
PlayersOnField *int `gorm:"column:players_on_field" json:"playersOnField"`
PlayersJSON json.RawMessage `gorm:"column:players_json;type:jsonb" json:"players" swaggertype:"array,object"`
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"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
}
func (mf *MatchFormation) BeforeCreate(tx *gorm.DB) (err error) {
if mf.ID != "" {
return nil
}
id, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 15)
if err != nil {
return err
}
mf.ID = id
return nil
}
type PlayerTransfer struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
WyID int `gorm:"column:wy_id;uniqueIndex" json:"wyId"`
TsID int `gorm:"column:ts_id;uniqueIndex" json:"tsId"`
PlayerWyID *int `gorm:"column:player_wy_id" json:"playerWyId"`
PlayerTsID *int `gorm:"column:player_ts_id" json:"playerTsId"`
FromTeamWyID *int `gorm:"column:from_team_wy_id" json:"fromTeamWyId"`
FromTeamTsID *int `gorm:"column:from_team_ts_id" json:"fromTeamTsId"`
ToTeamWyID *int `gorm:"column:to_team_wy_id" json:"toTeamWyId"`
ToTeamTsID *int `gorm:"column:to_team_ts_id" json:"toTeamTsId"`
FromTeamName *string `gorm:"column:from_team_name" json:"fromTeamName"`
ToTeamName *string `gorm:"column:to_team_name" json:"toTeamName"`
TransferDate *time.Time `gorm:"column:transfer_date" json:"transferDate"`
EndDate *time.Time `gorm:"column:end_date" json:"endDate"`
TransferType *string `gorm:"column:transfer_type" json:"transferType"`
TransferFee *float64 `gorm:"column:transfer_fee" json:"transferFee"`
Currency string `gorm:"column:currency;default:EUR" json:"currency"`
ContractLength *int `gorm:"column:contract_length" json:"contractLength"`
Season *string `gorm:"column:season" json:"season"`
IsActive bool `gorm:"column:is_active;default:false" json:"isActive"`
IsLoan bool `gorm:"column:is_loan;default:false" json:"isLoan"`
LoanDuration *int `gorm:"column:loan_duration" json:"loanDuration"`
HasOptionToBuy bool `gorm:"column:has_option_to_buy;default:false" json:"hasOptionToBuy"`
OptionToBuyFee *float64 `gorm:"column:option_to_buy_fee" json:"optionToBuyFee"`
AnnouncementDate *time.Time `gorm:"column:announcement_date" json:"announcementDate"`
SourceURL *string `gorm:"column:source_url" json:"sourceUrl"`
APILastSyncedAt *time.Time `gorm:"column:api_last_synced_at" json:"apiLastSyncedAt"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
}
type TeamSquad struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
TeamWyID *int `gorm:"column:team_wy_id" json:"teamWyId"`
TeamTsID *int `gorm:"column:team_ts_id" json:"teamTsId"`
PlayerWyID *int `gorm:"column:player_wy_id" json:"playerWyId"`
PlayerTsID *int `gorm:"column:player_ts_id" json:"playerTsId"`
PlayerWyIDExternal int `gorm:"column:player_wy_id_external" json:"playerWyIdExternal"`
IsActive bool `gorm:"column:is_active;default:true" json:"isActive"`
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"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
}
type Standing struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
CompetitionWyID *int `gorm:"column:competition_wy_id" json:"competitionWyId"`
CompetitionTsID *int `gorm:"column:competition_ts_id" json:"competitionTsId"`
SeasonWyID *int `gorm:"column:season_wy_id" json:"seasonWyId"`
SeasonTsID *int `gorm:"column:season_ts_id" json:"seasonTsId"`
TeamWyID *int `gorm:"column:team_wy_id" json:"teamWyId"`
TeamTsID *int `gorm:"column:team_ts_id" json:"teamTsId"`
GroupName string `gorm:"column:group_name;default:Regular Season" json:"groupName"`
Position *int `gorm:"column:position" json:"position"`
TotalPoints int `gorm:"column:total_points;default:0" json:"totalPoints"`
TotalPlayed int `gorm:"column:total_played;default:0" json:"totalPlayed"`
TotalWins int `gorm:"column:total_wins;default:0" json:"totalWins"`
TotalDraws int `gorm:"column:total_draws;default:0" json:"totalDraws"`
TotalLosses int `gorm:"column:total_losses;default:0" json:"totalLosses"`
TotalGoalsFor int `gorm:"column:total_goals_for;default:0" json:"totalGoalsFor"`
TotalGoalsAgainst int `gorm:"column:total_goals_against;default:0" json:"totalGoalsAgainst"`
GoalDifference int `gorm:"column:goal_difference;default:0" json:"goalDifference"`
PointsPerGame float64 `gorm:"column:points_per_game" json:"pointsPerGame"`
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"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at" json:"deletedAt"`
}
func (s *Standing) BeforeCreate(tx *gorm.DB) (err error) {
if s.ID != "" {
return nil
}
id, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 15)
if err != nil {
return err
}
s.ID = id
return nil
}
// Round mirrors the Nest/Drizzle `rounds` table.
type Round struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
WyID int `gorm:"column:wy_id;uniqueIndex" json:"wyId"`
TsID *int `gorm:"column:ts_id" json:"tsId"`
CompetitionWyID *int `gorm:"column:competition_wy_id" json:"competitionWyId"`
CompetitionTsID *int `gorm:"column:competition_ts_id" json:"competitionTsId"`
SeasonWyID *int `gorm:"column:season_wy_id" json:"seasonWyId"`
SeasonTsID *int `gorm:"column:season_ts_id" json:"seasonTsId"`
Name string `gorm:"column:name" json:"name"`
RoundType string `gorm:"column:round_type;type:round_type;default:regular" json:"roundType"`
RoundNumber *int `gorm:"column:round_number" json:"roundNumber"`
StartDate *time.Time `gorm:"column:start_date" json:"startDate"`
EndDate *time.Time `gorm:"column:end_date" json:"endDate"`
IsActive bool `gorm:"column:is_active;default:true" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at" json:"deletedAt"`
}
package models
import (
"time"
gonanoid "github.com/matoous/go-nanoid/v2"
"gorm.io/gorm"
)
type SampleRecord struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (s *SampleRecord) BeforeCreate(tx *gorm.DB) (err error) {
if s.ID != "" {
return nil
}
id, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 15)
if err != nil {
return err
}
s.ID = id
return nil
}
package router
import (
"encoding/base64"
"net/http"
"os"
"strings"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
_ "ScoutingSystemScoreData/docs"
"ScoutingSystemScoreData/internal/config"
"ScoutingSystemScoreData/internal/handlers"
)
func decodeEnvMaybeBase64(raw string) string {
raw = strings.TrimSpace(raw)
raw = strings.Trim(raw, "\"'")
if raw == "" {
return raw
}
decoded, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return raw
}
return string(decoded)
}
func basicAuthMiddleware() gin.HandlerFunc {
rawUser := os.Getenv("API_USERNAME")
rawPass := os.Getenv("API_PASSWORD")
username := decodeEnvMaybeBase64(rawUser)
password := decodeEnvMaybeBase64(rawPass)
// If not configured, do not enforce auth.
if username == "" && password == "" {
return func(c *gin.Context) { c.Next() }
}
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if !strings.HasPrefix(authHeader, "Basic ") {
c.Header("WWW-Authenticate", "Basic")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Basic authentication required"})
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[1] == "" {
c.Header("WWW-Authenticate", "Basic")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid authentication credentials"})
return
}
decoded, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
c.Header("WWW-Authenticate", "Basic")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid authentication credentials"})
return
}
creds := string(decoded)
up := strings.SplitN(creds, ":", 2)
if len(up) != 2 {
c.Header("WWW-Authenticate", "Basic")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid authentication credentials"})
return
}
if up[0] != username || up[1] != password {
c.Header("WWW-Authenticate", "Basic")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
return
}
c.Next()
}
}
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 {
c.JSON(http.StatusServiceUnavailable, gin.H{"status": "unhealthy", "error": err.Error()})
return
}
}
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, ginSwagger.PersistAuthorization(true)))
r.GET("/docs.html", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/swagger/index.html")
})
r.GET("/docs", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/swagger/index.html")
})
api := r.Group("/api")
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)
handlers.RegisterRefereeRoutes(api, db)
handlers.RegisterMatchRoutes(api, db)
handlers.RegisterStandingRoutes(api, db)
handlers.RegisterImportRoutes(api, db, appCfg)
return r
}
package services
import (
"context"
"strconv"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/errors"
"ScoutingSystemScoreData/internal/models"
)
type ListAreasOptions struct {
Limit int
Offset int
Name string
Alpha2 string
Alpha3 string
}
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)
GetByWyIDs(ctx context.Context, wyIDs []int) ([]models.Area, error)
}
type areaService struct {
db *gorm.DB
}
func NewAreaService(db *gorm.DB) AreaService {
return &areaService{db: db}
}
func (s *areaService) ListAreas(ctx context.Context, opts ListAreasOptions) ([]models.Area, int64, error) {
var areas []models.Area
query := s.db.WithContext(ctx).Model(&models.Area{})
if opts.Name != "" {
query = query.Where("name ILIKE ?", "%"+opts.Name+"%")
} else if opts.Alpha2 != "" {
query = query.Where("alpha2code = ?", opts.Alpha2)
} else if opts.Alpha3 != "" {
query = query.Where("alpha3code = ?", opts.Alpha3)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to count areas", err)
}
if err := query.Limit(opts.Limit).Offset(opts.Offset).Find(&areas).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to fetch areas", err)
}
return areas, total, nil
}
func (s *areaService) GetByID(ctx context.Context, id string) (models.Area, error) {
var area models.Area
if err := s.db.WithContext(ctx).First(&area, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Area{}, errors.New(errors.CodeNotFound, "area not found")
}
return models.Area{}, errors.Wrap(errors.CodeInternal, "failed to fetch area", err)
}
return area, nil
}
func (s *areaService) GetByProviderID(ctx context.Context, providerID string) (models.Area, error) {
var area models.Area
// If providerID is numeric, treat as wy_id; otherwise, treat as ts_id
if _, err := strconv.Atoi(providerID); err == nil {
if err := s.db.WithContext(ctx).First(&area, "wy_id = ?", providerID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Area{}, errors.New(errors.CodeNotFound, "area not found")
}
return models.Area{}, errors.Wrap(errors.CodeInternal, "failed to fetch area", err)
}
return area, nil
}
if err := s.db.WithContext(ctx).First(&area, "ts_id = ?", providerID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Area{}, errors.New(errors.CodeNotFound, "area not found")
}
return models.Area{}, errors.Wrap(errors.CodeInternal, "failed to fetch area", err)
}
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
}
func (s *areaService) GetByWyIDs(ctx context.Context, wyIDs []int) ([]models.Area, error) {
if len(wyIDs) == 0 {
return []models.Area{}, nil
}
wyIDStrs := make([]string, 0, len(wyIDs))
for _, id := range wyIDs {
if id > 0 {
wyIDStrs = append(wyIDStrs, strconv.Itoa(id))
}
}
if len(wyIDStrs) == 0 {
return []models.Area{}, nil
}
var areas []models.Area
if err := s.db.WithContext(ctx).Where("wy_id IN ?", wyIDStrs).Find(&areas).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternal, "failed to fetch areas by wy_id", err)
}
return areas, nil
}
package services
import (
"context"
"strconv"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/errors"
"ScoutingSystemScoreData/internal/models"
)
type CoachService interface {
ListCoaches(ctx context.Context, opts ListCoachesOptions) ([]models.Coach, int64, error)
GetByID(ctx context.Context, id string) (models.Coach, error)
GetByWyID(ctx context.Context, wyID int) (models.Coach, error)
GetByAnyProviderID(ctx context.Context, providerID string) (models.Coach, error)
}
type ListCoachesOptions struct {
Limit int
Offset int
Name string
TeamID string
Position string
ActiveOnly bool
}
type coachService struct {
db *gorm.DB
}
func NewCoachService(db *gorm.DB) CoachService {
return &coachService{db: db}
}
func (s *coachService) ListCoaches(ctx context.Context, opts ListCoachesOptions) ([]models.Coach, int64, error) {
var coaches []models.Coach
query := s.db.WithContext(ctx).Model(&models.Coach{})
if opts.Name != "" {
like := "%" + opts.Name + "%"
query = query.Where(
"first_name ILIKE ? OR last_name ILIKE ? OR middle_name ILIKE ? OR short_name ILIKE ?",
like, like, like, like,
)
} else if opts.TeamID != "" {
query = query.Where("current_team_wy_id = ?", opts.TeamID)
} else if opts.Position != "" {
query = query.Where("position = ?", opts.Position)
}
if opts.ActiveOnly {
query = query.Where("is_active = ?", true)
}
query = query.Order("is_active DESC")
query = query.Order("CASE WHEN current_team_wy_id IS NULL THEN 1 ELSE 0 END")
query = query.Order("CASE WHEN years_experience IS NULL THEN 1 ELSE 0 END")
query = query.Order("years_experience DESC")
query = query.Order("last_name ASC")
query = query.Order("first_name ASC")
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to count coaches", err)
}
if err := query.Limit(opts.Limit).Offset(opts.Offset).Find(&coaches).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to fetch coaches", err)
}
return coaches, total, nil
}
func (s *coachService) GetByID(ctx context.Context, id string) (models.Coach, error) {
var coach models.Coach
if err := s.db.WithContext(ctx).First(&coach, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Coach{}, errors.New(errors.CodeNotFound, "coach not found")
}
return models.Coach{}, errors.Wrap(errors.CodeInternal, "failed to fetch coach", err)
}
return coach, nil
}
func (s *coachService) GetByAnyProviderID(ctx context.Context, providerID string) (models.Coach, error) {
if wyID, err := strconv.Atoi(providerID); err == nil {
return s.GetByWyID(ctx, wyID)
}
var coach models.Coach
if err := s.db.WithContext(ctx).First(&coach, "ts_id = ?", providerID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Coach{}, errors.New(errors.CodeNotFound, "coach not found")
}
return models.Coach{}, errors.Wrap(errors.CodeInternal, "failed to fetch coach", err)
}
return coach, nil
}
func (s *coachService) GetByWyID(ctx context.Context, wyID int) (models.Coach, error) {
var coach models.Coach
if err := s.db.WithContext(ctx).First(&coach, "wy_id = ?", wyID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Coach{}, errors.New(errors.CodeNotFound, "coach not found")
}
return models.Coach{}, errors.Wrap(errors.CodeInternal, "failed to fetch coach", err)
}
return coach, 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, name *string, areaWyID *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, name *string, areaWyID *int) ([]models.Competition, int64, error) {
var competitions []models.Competition
query := s.db.WithContext(ctx).Model(&models.Competition{})
if name != nil && *name != "" {
query = query.Where("name ILIKE ?", "%"+*name+"%")
}
if areaWyID != nil && *areaWyID > 0 {
query = query.Where("area_wy_id = ?", *areaWyID)
}
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
}
package services
import (
"context"
"fmt"
"time"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/errors"
"ScoutingSystemScoreData/internal/models"
)
type MatchService interface {
ListMatches(ctx context.Context, opts ListMatchesOptions) ([]models.Match, int64, error)
GetByID(ctx context.Context, id string) (models.Match, error)
GetByWyID(ctx context.Context, wyID int) (models.Match, error)
GetByTsID(ctx context.Context, matchTsID string) (models.Match, error)
GetHeadToHeadMostRecent(ctx context.Context, teamHomeWyID int, teamAwayWyID int, from *time.Time, to *time.Time) (models.Match, error)
ListHeadToHead(ctx context.Context, teamHomeWyID int, teamAwayWyID int, from *time.Time, to *time.Time, limit int, offset int) ([]models.Match, int64, error)
GetLineup(ctx context.Context, matchTsID string) (models.Match, []models.MatchTeam, []models.MatchLineupPlayer, error)
GetLineupByID(ctx context.Context, id string) (models.Match, []models.MatchTeam, []models.MatchLineupPlayer, error)
GetLineupByWyID(ctx context.Context, wyID int) (models.Match, []models.MatchTeam, []models.MatchLineupPlayer, error)
}
type ListMatchesOptions struct {
Limit int
Offset int
CompetitionWyID *int
SeasonWyID *int
TeamTsID *string
TeamWyID *int
TeamSide *string // home|away|either
FromDate *time.Time
ToDate *time.Time
Status *string
Order string // asc|desc
}
type matchService struct {
db *gorm.DB
}
func NewMatchService(db *gorm.DB) MatchService {
return &matchService{db: db}
}
func (s *matchService) ListMatches(ctx context.Context, opts ListMatchesOptions) ([]models.Match, int64, error) {
var matches []models.Match
query := s.db.WithContext(ctx).Model(&models.Match{})
if opts.CompetitionWyID != nil {
query = query.Where("competition_wy_id = ?", *opts.CompetitionWyID)
}
if opts.SeasonWyID != nil {
query = query.Where("season_wy_id = ?", *opts.SeasonWyID)
}
teamSide := "either"
if opts.TeamSide != nil && *opts.TeamSide != "" {
teamSide = *opts.TeamSide
}
if teamSide != "home" && teamSide != "away" && teamSide != "either" {
return nil, 0, errors.New(errors.CodeInvalidInput, "invalid teamSide (use home, away, or either)")
}
if opts.TeamWyID != nil {
if teamSide == "home" {
query = query.Where("home_team_wy_id = ?", *opts.TeamWyID)
} else if teamSide == "away" {
query = query.Where("away_team_wy_id = ?", *opts.TeamWyID)
} else {
query = query.Where("home_team_wy_id = ? OR away_team_wy_id = ?", *opts.TeamWyID, *opts.TeamWyID)
}
} else if opts.TeamTsID != nil && *opts.TeamTsID != "" {
if teamSide == "home" {
query = query.Where("home_team_ts_id = ?", *opts.TeamTsID)
} else if teamSide == "away" {
query = query.Where("away_team_ts_id = ?", *opts.TeamTsID)
} else {
query = query.Where("home_team_ts_id = ? OR away_team_ts_id = ?", *opts.TeamTsID, *opts.TeamTsID)
}
}
if opts.FromDate != nil {
query = query.Where("match_date >= ?", *opts.FromDate)
}
if opts.ToDate != nil {
query = query.Where("match_date <= ?", *opts.ToDate)
}
if opts.Status != nil && *opts.Status != "" {
query = query.Where("status = ?", *opts.Status)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to count matches", err)
}
order := "match_date desc"
if opts.Order == "asc" {
order = "match_date asc"
}
if err := query.Order(order).Limit(opts.Limit).Offset(opts.Offset).Find(&matches).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to fetch matches", err)
}
return matches, total, nil
}
func (s *matchService) GetByTsID(ctx context.Context, matchTsID string) (models.Match, error) {
var match models.Match
if err := s.db.WithContext(ctx).First(&match, "ts_id = ?", matchTsID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Match{}, errors.New(errors.CodeNotFound, "match not found")
}
return models.Match{}, errors.Wrap(errors.CodeInternal, "failed to fetch match", err)
}
return match, nil
}
func (s *matchService) GetByID(ctx context.Context, id string) (models.Match, error) {
var match models.Match
if err := s.db.WithContext(ctx).First(&match, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Match{}, errors.New(errors.CodeNotFound, "match not found")
}
return models.Match{}, errors.Wrap(errors.CodeInternal, "failed to fetch match", err)
}
return match, nil
}
func (s *matchService) GetByWyID(ctx context.Context, wyID int) (models.Match, error) {
var match models.Match
if err := s.db.WithContext(ctx).First(&match, "wy_id = ?", wyID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Match{}, errors.New(errors.CodeNotFound, "match not found")
}
return models.Match{}, errors.Wrap(errors.CodeInternal, "failed to fetch match", err)
}
return match, nil
}
func (s *matchService) GetHeadToHeadMostRecent(ctx context.Context, teamHomeWyID int, teamAwayWyID int, from *time.Time, to *time.Time) (models.Match, error) {
var match models.Match
q := s.db.WithContext(ctx).Model(&models.Match{}).
Where("home_team_wy_id = ? AND away_team_wy_id = ?", teamHomeWyID, teamAwayWyID).
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, fmt.Sprintf("match not found for home=%d away=%d", teamHomeWyID, teamAwayWyID))
}
return models.Match{}, errors.Wrap(errors.CodeInternal, "failed to fetch head-to-head match", err)
}
return match, nil
}
func (s *matchService) ListHeadToHead(ctx context.Context, teamHomeWyID int, teamAwayWyID int, from *time.Time, to *time.Time, limit int, offset int) ([]models.Match, int64, error) {
var matches []models.Match
q := s.db.WithContext(ctx).Model(&models.Match{}).
Where("home_team_wy_id = ? AND away_team_wy_id = ?", teamHomeWyID, teamAwayWyID)
if from != nil {
q = q.Where("match_date >= ?", *from)
}
if to != nil {
q = q.Where("match_date <= ?", *to)
}
var total int64
if err := q.Count(&total).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to count head-to-head matches", err)
}
if err := q.Order("match_date desc").Limit(limit).Offset(offset).Find(&matches).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to fetch head-to-head matches", err)
}
return matches, total, nil
}
func (s *matchService) GetLineup(ctx context.Context, matchTsID string) (models.Match, []models.MatchTeam, []models.MatchLineupPlayer, error) {
match, err := s.GetByTsID(ctx, matchTsID)
if err != nil {
return models.Match{}, nil, nil, err
}
var teams []models.MatchTeam
if err := s.db.WithContext(ctx).Where("match_ts_id = ?", matchTsID).Order("side asc").Find(&teams).Error; err != nil {
return models.Match{}, nil, nil, errors.Wrap(errors.CodeInternal, "failed to fetch match teams", err)
}
var lineup []models.MatchLineupPlayer
if err := s.db.WithContext(ctx).
Where("match_ts_id = ?", matchTsID).
Order("is_starter desc").
Order("sub_order asc").
Order("shirt_number asc").
Find(&lineup).Error; err != nil {
return models.Match{}, nil, nil, errors.Wrap(errors.CodeInternal, "failed to fetch lineup players", err)
}
return match, teams, lineup, nil
}
func (s *matchService) GetLineupByID(ctx context.Context, id string) (models.Match, []models.MatchTeam, []models.MatchLineupPlayer, error) {
match, err := s.GetByID(ctx, id)
if err != nil {
return models.Match{}, nil, nil, err
}
if match.TsID == nil || *match.TsID == "" {
return models.Match{}, nil, nil, errors.New(errors.CodeNotFound, "match not found")
}
return s.GetLineup(ctx, *match.TsID)
}
func (s *matchService) GetLineupByWyID(ctx context.Context, wyID int) (models.Match, []models.MatchTeam, []models.MatchLineupPlayer, error) {
match, err := s.GetByWyID(ctx, wyID)
if err != nil {
return models.Match{}, nil, nil, err
}
if match.TsID == nil || *match.TsID == "" {
return models.Match{}, nil, nil, errors.New(errors.CodeNotFound, "match not found")
}
return s.GetLineup(ctx, *match.TsID)
}
package services
import (
"context"
"strconv"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/errors"
"ScoutingSystemScoreData/internal/models"
)
type PlayerService interface {
ListPlayers(ctx context.Context, opts ListPlayersOptions) ([]models.Player, int64, error)
GetByID(ctx context.Context, id string) (models.Player, error)
GetByProviderID(ctx context.Context, wyID int) (models.Player, error)
GetByAnyProviderID(ctx context.Context, providerID string) (models.Player, error)
}
type ListPlayersOptions struct {
Limit int
Offset int
Name string
TeamID string
Country string
}
type playerService struct {
db *gorm.DB
}
func NewPlayerService(db *gorm.DB) PlayerService {
return &playerService{db: db}
}
func (s *playerService) ListPlayers(ctx context.Context, opts ListPlayersOptions) ([]models.Player, int64, error) {
var players []models.Player
query := s.db.WithContext(ctx).Model(&models.Player{}).Where("is_active = ?", true)
if opts.Name != "" {
likePattern := "%" + opts.Name + "%"
query = query.Where(
"short_name ILIKE ? OR first_name ILIKE ? OR middle_name ILIKE ? OR last_name ILIKE ?",
likePattern, likePattern, likePattern, likePattern,
)
query = query.Order("CASE WHEN market_value IS NULL THEN 1 ELSE 0 END")
query = query.Order("market_value DESC")
query = query.Order("CASE WHEN current_national_team_id IS NULL THEN 1 ELSE 0 END")
query = query.Order("CASE WHEN current_team_id IS NULL AND (team_ts_id IS NULL OR team_ts_id = '') THEN 1 ELSE 0 END")
query = query.Order("last_name ASC")
query = query.Order("first_name ASC")
} else if opts.TeamID != "" {
query = query.Where("current_team_id = ?", opts.TeamID)
} else if opts.Country != "" {
query = query.Joins("JOIN areas ON areas.wy_id = players.birth_area_wy_id").Where("areas.name = ?", opts.Country)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to count players", err)
}
if err := query.Limit(opts.Limit).Offset(opts.Offset).Find(&players).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to fetch players", err)
}
return players, total, nil
}
func (s *playerService) GetByID(ctx context.Context, id string) (models.Player, error) {
var player models.Player
if err := s.db.WithContext(ctx).First(&player, "id = ?", id).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
}
func (s *playerService) GetByAnyProviderID(ctx context.Context, providerID string) (models.Player, error) {
// If providerID is numeric, treat as wy_id; otherwise, treat as ts_id
if wyID, err := strconv.Atoi(providerID); err == nil {
return s.GetByProviderID(ctx, wyID)
}
var player models.Player
if err := s.db.WithContext(ctx).First(&player, "ts_id = ?", providerID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
if err := s.db.WithContext(ctx).First(&player, "uid = ?", providerID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Player{}, errors.New(errors.CodeNotFound, "player not found")
}
return models.Player{}, errors.Wrap(errors.CodeInternal, "failed to fetch player", err)
}
return player, nil
}
return models.Player{}, errors.Wrap(errors.CodeInternal, "failed to fetch player", err)
}
return player, nil
}
func (s *playerService) GetByProviderID(ctx context.Context, wyID int) (models.Player, error) {
var player models.Player
if err := s.db.WithContext(ctx).First(&player, "wy_id = ?", wyID).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
}
package services
import (
"context"
"strconv"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/errors"
"ScoutingSystemScoreData/internal/models"
)
type RefereeService interface {
ListReferees(ctx context.Context, opts ListRefereesOptions) ([]models.Referee, int64, error)
GetByID(ctx context.Context, id string) (models.Referee, error)
GetByWyID(ctx context.Context, wyID int) (models.Referee, error)
GetByAnyProviderID(ctx context.Context, providerID string) (models.Referee, error)
}
type ListRefereesOptions struct {
Limit int
Offset int
Name string
CountryWyID *int
Type string
ActiveOnly bool
}
type refereeService struct {
db *gorm.DB
}
func NewRefereeService(db *gorm.DB) RefereeService {
return &refereeService{db: db}
}
func (s *refereeService) ListReferees(ctx context.Context, opts ListRefereesOptions) ([]models.Referee, int64, error) {
var referees []models.Referee
query := s.db.WithContext(ctx).Model(&models.Referee{})
if opts.Name != "" {
like := "%" + opts.Name + "%"
query = query.Where(
"first_name ILIKE ? OR last_name ILIKE ? OR middle_name ILIKE ? OR short_name ILIKE ?",
like, like, like, like,
)
}
if opts.CountryWyID != nil {
query = query.Where("nationality_wy_id = ?", *opts.CountryWyID)
}
if opts.Type != "" {
query = query.Where("referee_type = ?", opts.Type)
}
if opts.ActiveOnly {
query = query.Where("is_active = ?", true)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to count referees", err)
}
if err := query.Limit(opts.Limit).Offset(opts.Offset).Find(&referees).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to fetch referees", err)
}
return referees, total, nil
}
func (s *refereeService) GetByID(ctx context.Context, id string) (models.Referee, error) {
var referee models.Referee
if err := s.db.WithContext(ctx).First(&referee, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Referee{}, errors.New(errors.CodeNotFound, "referee not found")
}
return models.Referee{}, errors.Wrap(errors.CodeInternal, "failed to fetch referee", err)
}
return referee, nil
}
func (s *refereeService) GetByAnyProviderID(ctx context.Context, providerID string) (models.Referee, error) {
if wyID, err := strconv.Atoi(providerID); err == nil {
return s.GetByWyID(ctx, wyID)
}
var referee models.Referee
if err := s.db.WithContext(ctx).First(&referee, "ts_id = ?", providerID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Referee{}, errors.New(errors.CodeNotFound, "referee not found")
}
return models.Referee{}, errors.Wrap(errors.CodeInternal, "failed to fetch referee", err)
}
return referee, nil
}
func (s *refereeService) GetByWyID(ctx context.Context, wyID int) (models.Referee, error) {
var referee models.Referee
if err := s.db.WithContext(ctx).First(&referee, "wy_id = ?", wyID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Referee{}, errors.New(errors.CodeNotFound, "referee not found")
}
return models.Referee{}, errors.Wrap(errors.CodeInternal, "failed to fetch referee", err)
}
return referee, nil
}
package services
import (
"context"
"strconv"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/errors"
"ScoutingSystemScoreData/internal/models"
)
type SeasonService interface {
ListSeasons(ctx context.Context, limit, offset int, name *string) ([]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, name *string) ([]models.Season, int64, error) {
var seasons []models.Season
query := s.db.WithContext(ctx).Model(&models.Season{})
if name != nil && *name != "" {
query = query.Where("name ILIKE ?", "%"+*name+"%")
}
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
}
package services
import (
"context"
"strconv"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/errors"
"ScoutingSystemScoreData/internal/models"
)
type TeamService interface {
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)
GetByGsmIDs(ctx context.Context, gsmIDs []int) ([]models.Team, error)
GetByWyIDs(ctx context.Context, wyIDs []int) ([]models.Team, error)
}
type teamService struct {
db *gorm.DB
}
func NewTeamService(db *gorm.DB) TeamService {
return &teamService{db: db}
}
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+"%")
}
query = query.Order("CASE WHEN market_value IS NULL THEN 1 ELSE 0 END")
query = query.Order("market_value DESC")
query = query.Order("is_active DESC")
query = query.Order("CASE WHEN competition_ts_id IS NULL OR competition_ts_id = '' THEN 1 ELSE 0 END")
query = query.Order("CASE WHEN league IS NULL OR league = '' THEN 1 ELSE 0 END")
query = query.Order("name ASC")
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to count teams", err)
}
if err := query.Limit(limit).Offset(offset).Find(&teams).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to fetch teams", err)
}
return teams, total, nil
}
func (s *teamService) GetByID(ctx context.Context, id string) (models.Team, error) {
var team models.Team
if err := s.db.WithContext(ctx).First(&team, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Team{}, errors.New(errors.CodeNotFound, "team not found")
}
return models.Team{}, errors.Wrap(errors.CodeInternal, "failed to fetch team", err)
}
return team, nil
}
func (s *teamService) GetByAnyProviderID(ctx context.Context, providerID string) (models.Team, 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 team models.Team
if err := s.db.WithContext(ctx).First(&team, "ts_id = ?", providerID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Team{}, errors.New(errors.CodeNotFound, "team not found")
}
return models.Team{}, errors.Wrap(errors.CodeInternal, "failed to fetch team", err)
}
return team, nil
}
func (s *teamService) GetByWyID(ctx context.Context, wyID int) (models.Team, error) {
var team models.Team
if err := s.db.WithContext(ctx).First(&team, "wy_id = ?", wyID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Team{}, errors.New(errors.CodeNotFound, "team not found")
}
return models.Team{}, errors.Wrap(errors.CodeInternal, "failed to fetch 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
}
func (s *teamService) GetByWyIDs(ctx context.Context, wyIDs []int) ([]models.Team, error) {
if len(wyIDs) == 0 {
return []models.Team{}, nil
}
var teams []models.Team
if err := s.db.WithContext(ctx).Where("wy_id IN ?", wyIDs).Find(&teams).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternal, "failed to fetch teams by wy_id", err)
}
return teams, nil
}
func (s *teamService) GetByGsmIDs(ctx context.Context, gsmIDs []int) ([]models.Team, error) {
if len(gsmIDs) == 0 {
return []models.Team{}, nil
}
var teams []models.Team
if err := s.db.WithContext(ctx).Where("gsm_id IN ?", gsmIDs).Find(&teams).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternal, "failed to fetch teams by gsm_id", err)
}
return teams, nil
}
-- Migration 0001: add TheSports-related fields to players
-- Also rename players.team_wy_id -> team_provider_id to match new Go field.
ALTER TABLE players
RENAME COLUMN team_wy_id TO team_provider_id;
ALTER TABLE players
ADD COLUMN IF NOT EXISTS market_value integer,
ADD COLUMN IF NOT EXISTS market_value_currency varchar(16),
ADD COLUMN IF NOT EXISTS contract_until timestamp with time zone,
ADD COLUMN IF NOT EXISTS ability_json jsonb,
ADD COLUMN IF NOT EXISTS characteristics_json jsonb,
ADD COLUMN IF NOT EXISTS uid text,
ADD COLUMN IF NOT EXISTS deathday timestamp with time zone,
ADD COLUMN IF NOT EXISTS retire_time timestamp with time zone;
\ No newline at end of file
-- Migration 0002: add TheSports country id to areas
ALTER TABLE areas
ADD COLUMN IF NOT EXISTS ts_id varchar(64);
-- Migration 0003: add Phase 1 indexes for common filters and lookups
-- Players: common filters
CREATE INDEX IF NOT EXISTS idx_players_is_active ON players (is_active);
CREATE INDEX IF NOT EXISTS idx_players_current_team_id ON players (current_team_id);
CREATE INDEX IF NOT EXISTS idx_players_birth_area_wy_id ON players (birth_area_wy_id);
-- Areas: common lookups and join support (provider ids are optional, but indexed)
CREATE INDEX IF NOT EXISTS idx_areas_wy_id ON areas (wy_id);
CREATE INDEX IF NOT EXISTS idx_areas_ts_id ON areas (ts_id);
-- Teams: common lookups
CREATE INDEX IF NOT EXISTS idx_teams_ts_id ON teams (ts_id);
CREATE INDEX IF NOT EXISTS idx_teams_wy_id ON teams (wy_id);
-- Migration 0004: add indexes to support large-table match queries
-- Matches: most recent match between two teams.
-- Supports queries like:
-- WHERE home_team_ts_id = ? AND away_team_ts_id = ? ORDER BY match_date DESC LIMIT 1
-- and the reversed team order.
CREATE INDEX IF NOT EXISTS idx_matches_home_away_date_desc ON matches (home_team_ts_id, away_team_ts_id, match_date DESC);
CREATE INDEX IF NOT EXISTS idx_matches_away_home_date_desc ON matches (away_team_ts_id, home_team_ts_id, match_date DESC);
-- Matches: matches by competition (and date ordering).
-- Supports queries like:
-- WHERE competition_wy_id = ? ORDER BY match_date DESC LIMIT ?
CREATE INDEX IF NOT EXISTS idx_matches_competition_wy_date_desc ON matches (competition_wy_id, match_date DESC);
-- Matches: matches by season (common usage when competition has multiple seasons).
CREATE INDEX IF NOT EXISTS idx_matches_season_wy_date_desc ON matches (season_wy_id, match_date DESC);
-- Match lineup players: fast retrieval for a match lineup and for player appearance lookups.
CREATE INDEX IF NOT EXISTS idx_match_lineup_players_match_ts_id ON match_lineup_players (match_ts_id);
CREATE INDEX IF NOT EXISTS idx_match_lineup_players_team_ts_match_ts ON match_lineup_players (team_ts_id, match_ts_id);
CREATE INDEX IF NOT EXISTS idx_match_lineup_players_player_ts_id ON match_lineup_players (player_ts_id);
-- 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);
ALTER TABLE competitions
ADD COLUMN IF NOT EXISTS ts_category_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS country_ts_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS short_name TEXT,
ADD COLUMN IF NOT EXISTS logo TEXT,
ADD COLUMN IF NOT EXISTS ts_type INTEGER,
ADD COLUMN IF NOT EXISTS cur_season_ts_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS cur_stage_ts_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS cur_round INTEGER,
ADD COLUMN IF NOT EXISTS round_count INTEGER,
ADD COLUMN IF NOT EXISTS title_holder_json JSONB,
ADD COLUMN IF NOT EXISTS most_titles_json JSONB,
ADD COLUMN IF NOT EXISTS newcomers_json JSONB,
ADD COLUMN IF NOT EXISTS divisions_json JSONB,
ADD COLUMN IF NOT EXISTS host_json JSONB,
ADD COLUMN IF NOT EXISTS uid TEXT,
ADD COLUMN IF NOT EXISTS ts_updated_at TIMESTAMPTZ;
CREATE TABLE IF NOT EXISTS match_formations (
id VARCHAR(16) PRIMARY KEY,
match_wy_id INTEGER NOT NULL,
side TEXT NOT NULL,
wy_id INTEGER,
scheme TEXT,
start_sec INTEGER,
end_sec INTEGER,
match_period_start TEXT,
match_period_end TEXT,
team_wy_id INTEGER,
players_on_field INTEGER,
players_json JSONB,
api_last_synced_at TIMESTAMPTZ,
api_sync_status TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_match_formations_match_wy_id ON match_formations(match_wy_id);
CREATE INDEX IF NOT EXISTS idx_match_formations_team_wy_id ON match_formations(team_wy_id);
CREATE INDEX IF NOT EXISTS idx_match_formations_match_wy_id_side ON match_formations(match_wy_id, side);
ALTER TABLE match_formations
ADD CONSTRAINT match_formations_match_wy_id_side_wy_id_unique
UNIQUE (match_wy_id, side, wy_id);
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