Skip to content

🎫 [BUG] 500 INTERNAL_ERROR al reanudar torneo pausado β€” POST /scheduling/{id}/resumeΒ #97

Description

@santiagorodriguezg

🎫 [BUG] 500 INTERNAL_ERROR al reanudar torneo pausado β€” POST /scheduling/{id}/resume

SEVERIDAD: Alta β€” el torneo queda atascado en 'paused' sin poder reanudarse desde la UI.
ENDPOINT: POST /scheduling/{tournamentId}/resume (body vacΓ­o)
TORNEO: 65d1ecb9-78b2-4f45-83e8-4f9388daec86
TRACE ID: 3328f374-7436-4cff-92c5-e1ceeb18c0dd

⚠️ FRAMING: el backend es NestJS detrΓ‘s de Traefik (NO n8n; el dominio n8n.srv… se
comparte por routing). Logs en el contenedor tennis-management-backend (pino/JSON).

──────────────────────────────────────────────
ANÁLISIS (verificado en develop, código actual)
──────────────────────────────────────────────
El 500 es code:"INTERNAL_ERROR" = una EXCEPCIΓ“N CRUDA no controlada, y estΓ‘ en el
REFLOW de resume. Descartes con evidencia:

  • pausedAt SÍ se persiste al pausar (scheduling.service.ts:2233); y resume ya valida
    !paused_at con AppError('TOURNAMENT_PAUSE_INCONSISTENT',500) (2289-2294) β†’ cΓ³digo
    propio, NO el genΓ©rico. β†’ No es "pausedAt nulo".
  • Idempotencia ya cubierta: status != 'paused' β†’ 409 TOURNAMENT_NOT_PAUSED (2283-2288).
  • GlobalExceptionFilter mapea AppErrorβ†’cΓ³digo; P2002β†’409 UNIQUE_CONSTRAINT_VIOLATION;
    P2003β†’400; P2023β†’400; y TODO lo demΓ‘s β†’ INTERNAL_ERROR 500 (filter:238).
    β†’ Por descarte, el error es un Prisma no mapeado (P2010 raw query failed / P2025…) o
    un error JS puro (Invalid Date, null deref), dentro del bloque if (delayMinutes>0)
    (scheduling.service.ts:2305-2453).
  • resume es 1 transacciΓ³n atΓ³mica β†’ al reventar hace ROLLBACK β†’ el torneo queda 'paused'.
    "Una vez funcionΓ³, ahora no" = el 1er resume commiteΓ³ y cambiΓ³ el estado de los
    partidos; ese estado nuevo revienta el reflow. ES DEPENDIENTE DEL ESTADO (no del request).

CANDIDATOS (confirmar con el stack):
a) Escrituras del reflow (2359-2387): match_assignments.create con
scheduled_start_at:new Date(next.startsAt). Si el planner devuelve next con fecha
invΓ‘lida/undefined en algΓΊn borde β†’ Prisma error no mapeado β†’ 500.
(Doble-reserva de slot darΓ­a 409 UNIQUE_CONSTRAINT_VIOLATION, NO 500 β†’ no es eso.)
b) buildBulkReschedulePlan (2324) + helpers con $queryRaw (afectados/candidatos/regrid):
raw query que falla por el estado β†’ P2010 β†’ INTERNAL_ERROR.
c) NO perseguir el Γ‘ngulo "zona horaria/hoy en UTC": fallarΓ­a siempre, no es
state-dependent. todayStart usa buildDateTimeInTimezone con la tz del venue.

──────────────────────────────────────────────
PASO 1 (obligatorio) β€” CONSEGUIR EL STACK
──────────────────────────────────────────────
El filtro loguea la excepciΓ³n COMPLETA con stack junto al traceId
(global-exception.filter.ts:222-234, campo err, nivel pino 50).

tail de errores + reproducir el 500 en la UI (el torneo estaba 'paused'):

docker logs -f --tail 0 tennis-management-backend 2>&1 | grep '"level":50'

(jq opcional): ... | jq -r '.err.stack // .err.message'

⚠️ Cada deploy hace up --force-recreate y reinicia logs β†’ capturar traceId fresco.
El stack apunta a la lΓ­nea exacta.

──────────────────────────────────────────────
FIX REQUERIDO
──────────────────────────────────────────────
A) Causa raΓ­z exacta (vΓ­a stack) y corregir la lΓ­nea que revienta.
B) Blindar el reflow: envolver las escrituras del reflow; ante un fallo del reflow,
devolver un cΓ³digo manejable (p.ej. RESUME_REFLOW_FAILED, 409/422) en vez de 500;
guardar defensivamente contra next con fecha invΓ‘lida y slot_ids inexistentes.
Los partidos no colocables deben degradar a 'draft' (comportamiento ya existente),
nunca tumbar toda la operaciΓ³n con INTERNAL_ERROR.
C) NO re-implementar: pausedAt persistido + idempotencia 409 ya estΓ‘n.

CRITERIOS DE ACEPTACIΓ“N

  • /resume responde 200 (o un cΓ³digo de dominio manejable), nunca INTERNAL_ERROR.
  • Un reflow sin slots disponibles degrada a 'draft' / devuelve cΓ³digo claro.
  • El torneo queda siempre en estado consistente (reanudado o pausado vΓ‘lido).

DESATASCAR EL TORNEO (ya se puede hacer manualmente; ver SQL entregado por el PM):
UPDATE tenis.tournaments
SET status_slug=COALESCE(pre_pause_status_slug,'active'), paused_at=NULL,
pre_pause_status_slug=NULL
WHERE id='65d1ecb9-78b2-4f45-83e8-4f9388daec86';

DATO PARA EL FRONT: si se define RESUME_REFLOW_FAILED (u otro code), avisar para mapearlo
a un toast (hoy ya manejan 409 β†’ "El torneo no estΓ‘ en pausa").

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions