Goals — Behavior
State machine
create ──> active ──┬── progress crosses target ──> achieved (achieved_at set)
├── user edits target / direction (still active)
├── cancelled (manual)
└── soft-delete via DELETE → deleted_at set (UI calls this "archive")Invariants
| Invariant | Enforcement |
|---|---|
Exactly one of metricType or exerciseId is set | DB CHECK goals_type_target_chk |
Increase goal: startValue < targetValue | Service create |
Decrease goal: startValue > targetValue | Service create |
startValue == targetValue is invalid | Service create |
| Exercise referenced must be canonical or this org’s | findFirst with OR clause on organizationId IS NULL or = orgId |
| Members can only create / read / update their own goals | findMyGoals, update, remove resolve via authenticated membership |
Golden paths
Member creates a weight-loss goal
- Member opens goals screen. Picks “Lose weight to 72kg”.
- UI computes
direction='decrease'automatically (helperdefaultDirectionFor). POST /goals/mewith{ type: 'body_metric', metricType: 'weight', targetValue: '72', unit: 'kg', direction: 'decrease' }.- Service: validates target, resolves
startValuefrom latest body_metric (e.g. 78.5kg), inserts row withdirection='decrease',start=78.5,target=72. - Progress percentage computed via
computeProgress:(start - current) / (start - target) * 100capped at 100.
Member hits a back squat PR target
- Member set a goal: 1RM back squat to 150kg (
exercise_pr). - After completing a workout with a 152kg back squat, the personal_records table gets a new row.
- Next time goals are read,
enrichGoalre-computes progress; 152 ≥ 150 → status flips toachieved,achievedAt = now.
Member archives a stale goal
DELETE /goals/:id→ setsdeleted_at. Listing excludes.
Edge cases
| Scenario | Behavior |
|---|---|
| Member sets a goal with no current body_metric | startValueNumeric resolves to null (no baseline available); progress falls back to current / target ratio |
| Member changes the target downward to make the goal already achieved | Status flips to achieved on next enrich |
| Member edits direction post-creation | Allowed; old start/target combination may invalidate — service re-validates |
| Exercise PR for an org-custom exercise | Allowed; resolution uses personalRecords for that exercise |
| Member belongs to two orgs and creates “lose 5kg” in both | Two distinct rows (organization_id differs) |
| Coach views a member’s goals via member detail | Allowed; staff role check |
Side effects
| Action | Writes | Reads |
|---|---|---|
| Create | goals insert (resolves startValue from body_metrics / personal_records) | one of the latter two |
| Update | goals update | same |
| List | — (enrich computes progress on the fly) | body_metrics / personal_records for current value |
| Achievement detection | goals update (status, achievedAt) | same |
| Delete | goals update (deleted_at) | — |
No events emitted today.
Permissions
| Action | Auth |
|---|---|
| Member create / list / update / delete own | Self-scoped via requireMembership |
| Staff view member’s goals | Staff role + same org |