Skip to content

ADR-007: Challenge Timing Recording and Validation

Status

Accepted

Context

Challenges involve two kinds of time data: admin-defined catalog metadata (estimates and optional schedule windows) and per-user progress timestamps logged as the user works through an attempt. Touli must record enough timing data for audit and operations without blocking task completion on user-entered durations. Over-estimation of total challenge time must be caught once, at challenge completion, before points are awarded.

This ADR complements ADR-006, which covers when points are awarded (challenge completion only, not per task).

Decision

Separate timing recording from timing validation.

1. Timing recording (server-logged, no user input)

Record timestamps automatically; do not validate user-entered values at the task level.

LayerFieldsWhen set
Challenge attempt (user_challenges)acceptedAt, completedAtJoin; successful challenge completion
Task progress (user_task_progress)startedAt, completedAtFirst task on join; each task on complete; next task on unlock

Complete Task (POST /challenges/:id/tasks/:taskId/complete) requires only verification evidence (photoUrl, optional description). It updates progress and logs completedAt (and backfills startedAt if missing). No minutes field and no time validation on this step.

2. Timing validation (user-entered, challenge completion only)

Complete Challenge (POST /challenges/:id/complete) is the only step that validates user-entered time.

Rule:

minutes <= challenge.estimatedTime
  • If minutes exceeds challenge.estimatedTime, the submission is rejected with accepted: false, reason: EXCEEDS_ESTIMATED_TIME, and estimatedTime in the response. The client must ask the user to enter a value at or below the estimate.
  • Rejection is an expected business outcome (HTTP 200), not a hard error. A rejected challenge_submissions row is persisted with zero points.
  • Up to 3 rejected submissions per attempt (MAX_FAILED_SUBMISSION_ATTEMPTS); on the 3rd rejection the attempt is locked (CHALLENGE_LOCKED). The user may rejoin to start a fresh attempt.

Server-logged join and task timestamps are not used for this validation check.

3. Catalog timing (admin-defined, separate from validation)

EntityFieldsPurpose
challengesestimatedTime (required), startsAt, endsAt (optional)Challenge estimate used at completion validation; optional availability window
challenge_tasksdescription, estimatedTime, startsAt, endsAt (optional)Optional per-task catalog copy and display/schedule metadata

Only challenges.estimatedTime is enforced at completion. Optional task-level estimatedTime may be shown in API responses (task.estimatedTime ?? challenge.estimatedTime) but is not validated on Complete Task. Catalog startsAt / endsAt on challenges and tasks are stored for admin scheduling; sequential task unlock is not driven by those dates in the current implementation.

4. What we explicitly do not validate

  • Per-task entered duration — no user time entry on Complete Task; no comparison of task elapsed time to estimate or to server clocks.
  • Elapsed time since join — not compared to submitted minutes at challenge completion (only the challenge estimatedTime ceiling applies).
  • Catalog schedule windowsstartsAt / endsAt on tasks and challenges are not enforced on join, unlock, or complete in the current backend.

Consequences

  • Mobile Complete Task stays simple: photo + optional note; no per-task minutes field.
  • Mobile Complete Challenge must handle EXCEEDS_ESTIMATED_TIME, display estimatedTime, and show remaining attempts before lock.
  • Audit trail: acceptedAt / completedAt on the attempt and startedAt / completedAt per task support operational review without affecting points logic.
  • Mis-reported total challenge time is corrected at completion (up to 3 tries), not during individual tasks.
  • Catalog task/challenge schedule fields remain available for future gating or UI without changing the recording or validation model above.