POST /v2/match — Actualizar partido (v2)
Lambda: MatchPostV2
Handler: STACK_TORNEOS/services/match/post_v2.py
Autenticación: JWT Cognito (Authorization: Bearer <token>)
CORS: Habilitado (MatchV2Options en /v2/match OPTIONS)
Query Parameters
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
matchId | string | ✅ | ID del fixture a actualizar |
Descripción
Permite actualizar un partido de forma parcial o total. El front puede enviar sólo los campos que cambiaron, sin necesidad de reenviar el estado completo.
Todos los casos de entrada son combinables en un mismo body (excepto donde se indique).
Casos de entrada
Caso 1 — Actualizar UN solo jugador
Ideal para registrar un gol, tarjeta o sanción en tiempo real sin enviar todo el equipo.
POST /v2/match?matchId=35071
Authorization: Bearer <token>
{
"team_id": 15358,
"player": {
"id": 34038,
"goal": 2,
"yellow_card": 1,
"red_card": 0,
"assistence": 1,
"position": 9,
"penalty_round": 0,
"penalty_status": "no-penalty"
}
}
Campos del jugador:
| Campo | Tipo | Descripción |
|---|---|---|
id | integer | ID del userprofile. Si no viene, se resuelve por first_name + last_name |
first_name | string | Requerido solo si no hay id |
last_name | string | Requerido solo si no hay id |
goal | integer | Goles marcados por el jugador |
assistence | integer | Asistencias |
yellow_card | integer | Tarjetas amarillas |
red_card | integer | Tarjetas rojas |
position | integer | Dorsal o posición |
penalty_round | integer | Goles en definición por penales |
penalty_status | string | "goal" | "no-penalty" | "miss" |
⚠️ El total de goles del equipo se recalcula automáticamente desde la DB al hacer el upsert. No es necesario enviar los datos de los demás jugadores.
Caso 2 — Actualizar N jugadores de un equipo
Cuando se cierra la carga de stats de un equipo completo (sin necesidad de enviar el otro equipo).
POST /v2/match?matchId=35071
Authorization: Bearer <token>
{
"team_id": 15358,
"players": [
{ "id": 34038, "goal": 2, "yellow_card": 0, "red_card": 0, "assistence": 1, "position": 9 },
{ "id": 34039, "goal": 1, "yellow_card": 1, "red_card": 0, "assistence": 0, "position": 7 },
{ "id": 34059, "goal": 0, "yellow_card": 0, "red_card": 1, "assistence": 0, "position": 4 }
]
}
Caso 3 — Actualizar ambos equipos (compatibilidad v1)
Equivalente al contrato original. Soportado para no romper integraciones existentes.
POST /v2/match?matchId=35071
Authorization: Bearer <token>
{
"team_1": {
"team_id": 15358,
"players": [
{ "id": 34038, "goal": 2, "assistence": 1, "yellow_card": 0, "red_card": 0, "position": 9 },
{ "id": 34039, "goal": 0, "assistence": 0, "yellow_card": 1, "red_card": 0, "position": 7 }
]
},
"team_2": {
"team_id": 15350,
"players": [
{ "id": 33761, "goal": 1, "assistence": 0, "yellow_card": 0, "red_card": 0, "position": 11 }
]
}
}
Caso 4 — Partido con definición por penales
Se activa cuando al menos un jugador tiene penalty_round > 0.
POST /v2/match?matchId=35071
Authorization: Bearer <token>
{
"team_1": {
"team_id": 15358,
"players": [
{ "id": 34038, "goal": 1, "penalty_round": 1, "penalty_status": "goal" },
{ "id": 34039, "goal": 0, "penalty_round": 0, "penalty_status": "miss" }
]
},
"team_2": {
"team_id": 15350,
"players": [
{ "id": 33761, "goal": 1, "penalty_round": 1, "penalty_status": "goal" },
{ "id": 34161, "goal": 0, "penalty_round": 1, "penalty_status": "miss" }
]
}
}
El campo
penalty_round = TRUEy los totalesteam_1_penalty/team_2_penaltyse guardan en el fixture automáticamente.
Caso 5 — Jugador sin cuenta registrada (manual)
Cuando el jugador no tiene cuenta en la plataforma, se crea automáticamente.
POST /v2/match?matchId=35071
Authorization: Bearer <token>
{
"team_id": 15358,
"player": {
"first_name": "Diego",
"last_name": "García",
"goal": 1,
"yellow_card": 0,
"red_card": 0,
"assistence": 0,
"position": 10
}
}
Si no existe en
userprofileporfirst_name + last_name, se crea el perfil en PostgreSQL, se agrega a DynamoDB (Profiles{ENV}) y se vincula al equipo (team_userprofile).
Caso 6 — Actualizar firmas y foto del árbitro
POST /v2/match?matchId=35071
Authorization: Bearer <token>
{
"signature_1": "data:image/png;base64,iVBORw0KGgoAAAANS...",
"signature_2": "data:image/png;base64,iVBORw0KGgoAAAANS...",
"referee_image": "data:image/png;base64,iVBORw0KGgoAAAANS..."
}
| Campo | Descripción |
|---|---|
signature_1 | Firma del capitán del equipo 1 (base64) |
signature_2 | Firma del capitán del equipo 2 (base64) |
referee_image | Foto/firma del árbitro (base64) |
Caso 7 — Actualizar comentario y/o suspensión
POST /v2/match?matchId=35071
Authorization: Bearer <token>
{
"comment": "Partido suspendido por lluvia",
"suspended": true
}
| Campo | Tipo | Descripción |
|---|---|---|
comment | string | Comentario libre sobre el partido |
suspended | boolean | true si el partido fue suspendido |
Caso 8 — Cerrar partido (finished)
Marca el partido como jugado. Dispara la lógica de avance de fase en playoffs.
POST /v2/match?matchId=35071
Authorization: Bearer <token>
{
"finished": true
}
Acciones que se ejecutan:
UPDATE fixture SET playered = TRUE, date_finished = now()(idempotente: solo si aún no estaba jugado)- Si el torneo estaba en
preparation→ pasa ain_progress - Verifica si todos los partidos de la fase están jugados → actualiza fase y notificaciones de avance de playoffs
Caso 9 — Combinación de campos en un solo request
Se pueden combinar campos simples con datos de jugadores y el cierre en un mismo request.
POST /v2/match?matchId=35071
Authorization: Bearer <token>
{
"comment": "Gran partido",
"signature_1": "data:image/png;base64,iVBORw0...",
"team_id": 15358,
"players": [
{ "id": 34038, "goal": 2, "assistence": 1, "yellow_card": 0, "red_card": 0, "position": 9 },
{ "id": 34039, "goal": 0, "assistence": 0, "yellow_card": 1, "red_card": 0, "position": 7 }
],
"finished": true
}
Respuesta exitosa
HTTP 201
{
"error": false,
"data": {
"match": 35071,
"suspended": false,
"comment": "Gran partido",
"footballfield": "Cancha 2",
"id_league": null,
"tournament_name": "Liga Verano 2026",
"tournament_id": 3639,
"playoff_name": "Semi-final",
"tournament_friendly": false,
"playered": true,
"signature_1": "https://cdn.statics.dev.mas10.ar/fixture_signature_1/fileCXYV27MTJK.png",
"signature_2": "https://cdn.statics.dev.mas10.ar/fixture_signature_2/fileID082DGG61.png",
"referee_image": "",
"date_played": "2025-11-23 00:00",
"phase": "Semi-final",
"playoff": true,
"penalty_round": false,
"team_1_goal": 3,
"team_2_goal": 1,
"team_1_penalty": 0,
"team_2_penalty": 0,
"team_1": {
"team_id": 15358,
"username": "equipo2",
"team_name": "ASTON BIRRA",
"avatar": "https://cdn.statics.dev.mas10.ar/team_avatar/file_team1.png",
"avatar_50": "https://cdn.statics.dev.mas10.ar/team_avatar/file_team1_50x75.png",
"avatar_100": "https://cdn.statics.dev.mas10.ar/team_avatar/file_team1_100x100.png",
"goals": 3,
"topic_name": "team-15358Equipo2",
"players": [
{
"id": 34038,
"username": null,
"first_name": "Diego",
"last_name": "Teru",
"full_name": "Teru Diego",
"goal": 2,
"assistence": 1,
"yellow_card": 0,
"red_card": 0,
"position": 9,
"avatar": null,
"avatar_50": null,
"avatar_100": null,
"medicalreport": null,
"blocked_user": false,
"motive_blocked": null,
"penalty_round": null,
"penalty_status": null,
"validated": null,
"rejection": null,
"reason_rejection": null,
"id_league": null
}
]
},
"team_2": {
"team_id": 15350,
"username": "loscuatrodesiempre",
"team_name": "LOS CUATRO DE SIEMPRE",
"avatar": null,
"avatar_50": null,
"avatar_100": null,
"goal": 1,
"topic_name": "team-15350CUATROPERFILLIRICUS665",
"players": [
{
"id": 33761,
"username": null,
"first_name": "MANUAL",
"last_name": "LOQUESETECANTE II",
"full_name": "LOQUESETECANTE II MANUAL",
"goal": 1,
"assistence": 0,
"yellow_card": 0,
"red_card": 0,
"position": 0,
"avatar": null,
"avatar_50": null,
"avatar_100": null,
"medicalreport": null,
"blocked_user": false,
"motive_blocked": null,
"penalty_round": null,
"penalty_status": null,
"validated": null,
"rejection": null,
"reason_rejection": null,
"id_league": null
}
]
}
}
}
Respuestas de error
401 — Token inválido o expirado
{ "error": true, "message": "User not found" }
500 — Error interno (DB, DynamoDB, etc.)
El handler captura excepciones individuales por sección (jugadores, Postgres, DynamoDB) y las loguea sin abortar el request completo. Solo falla con 500 si el error es en la capa de autenticación o de respuesta final.
Headers de respuesta CORS
Incluidos automáticamente por locales.get_response_standar() y locales.get_response_options():
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type,Authorization,X-Language
Access-Control-Allow-Methods: OPTIONS,POST,GET,PUT,DELETE
Notas de implementación
| Aspecto | Comportamiento |
|---|---|
| Idempotencia | finished=true es idempotente: WHERE playered=FALSE evita doble procesamiento |
| Totales de goles | Se recalculan con SUM(goal) desde fixture_info en DB, no desde el payload |
| Jugador sin cuenta | Si no trae id, se resuelve o crea por first_name + last_name |
| Persistencia dual | Toda actualización va a PostgreSQL (fixture, fixture_info) y DynamoDB (cache) |
| NotasIA / Push | Eliminados del flujo síncrono en v2 (~240ms de mejora en finished=true) |
| Timeout | 15 segundos (vs 30s en v1) |
Comparativa de performance (warm)
| Caso | v1 | v2 |
|---|---|---|
| Firma sola | ~208ms | ~60ms |
finished: true | ~579ms | ~130-160ms |
| Comentario solo | ~24ms | ~15ms |
| Un jugador (gol) | ~200ms | ~50ms |