message: 403: {"code":"ENTITLEMENT_ERROR","message":"Error 403: the user is not subscribed to required model plan"}

stop_reason: error


error_message: 403: {"code":"ENTITLEMENT_ERROR","message":"Error 403: the user is not subscribed to required model plan"}


I read this as an audit of the post-consent nutrition onboarding and daily check-in customer UX - I’ll trace every user-facing step, then report severity-ranked friction, Korean replacement copy, and contract-safe input/button simplifications with file/line evidence.


I read this as an audit of the post-consent nutrition onboarding and daily check-in customer UX - I traced all 22 onboarding questions, all 13 daily steps, and the six conditional follow-ups. No files were edited.

## Severity-ranked friction

### P1 — Consent button is misleading
`내용 확인 후 동의` immediately records consent; it does not open a review step (`nutrition_coaching.py:3563-3579`, `telegram.py:6743-6777`).

**Replacement**
- `동의하고 영양 온보딩 시작`
- `동의하지 않기`

After consent, the current notice says check-in has not started, then immediately opens onboarding (`nutrition_coaching.py:3640-3647`). Use:

> 동의가 저장됐어요. 이제 영양 온보딩을 시작할게요.
> 일일 체크인과 코칭은 운영자 활성화 후 시작됩니다.

### P1 — Onboarding is 22 serial questions with no in-progress edit/back action
The contract contains 22 fields (`nutrition_onboarding_contract.py:13-35`), and collection only offers progress text plus typed answers (`telegram_nutrition_onboarding_runtime_publication.py:228-236`). Correction is only exposed at attestation (`telegram_nutrition_onboarding_copy.py:117-121`).

This creates abandonment risk and makes an early typo expensive.

**Recommendation:** add a visible `이전` action only if the domain gets a versioned previous transition. Do not implement client-only navigation. At minimum, expose `건너뛰기` as a button for optional fields; the existing server already accepts the exact text (`telegram_nutrition_onboarding_runtime_collection.py:243-250`).

### P1 — Four optional questions look mandatory
`activity_rationale`, `cooking_access`, `budget_band`, and `schedule_constraints` default to empty (`telegram_nutrition_onboarding.py:31-35`), but the customer must discover that typing `건너뛰기` is allowed (`telegram_nutrition_onboarding_runtime_publication.py:235-236`).

Use a one-tap `건너뛰기` button that sends the existing exact value.

### P1 — Daily check-in is input-heavy and has a missing normal digestion option
The daily flow has 13 steps (`wizard.py:188-196`). Digestion offers Bristol 1–7, gas/bloating, or “other,” but no visible normal option (`physique_checkin_prompts.py:79-95`). The underlying contract already supports `normal` (`wizard.py:1099-1100`).

This forces healthy users either to choose a stool type or type manually.

**Recommendation:** add `정상` mapped to the existing `normal` value; retain detailed Bristol options for users who need them.

### P1 — Daily training question contradicts the rest of the time frame
The button prompt says `오늘 운동 내용을` while the typed prompt asks about exercise done `어제` (`physique_checkin_prompts.py:108-113`, `:150-152`).

Use:

> 어제 실제로 한 운동이 있다면 부위·종목·시간을 적어 주세요. 없으면 `휴식`이라고 입력해 주세요.

### P2 — Scale instructions omit labels for 2 and 4
The UI explains only 1, 3, and 5 (`physique_checkin_prompts.py:65-77`), leaving 2 and 4 ambiguous.

Use:

> 1 매우 나쁨 · 2 나쁨 · 3 보통 · 4 좋음 · 5 매우 좋음

### P2 — Launcher labels are inconsistent
The persistent keyboard says `오늘 체크인`, while the inline card says `오늘 체크인 시작` (`telegram.py:6544-6566`, `:6618-6625`).

Use `오늘 체크인` consistently; retain `오늘 체크인 수정` as the only correction entry.

### P2 — Safety/review completion does not tell the customer what to do next
After attestation, the customer sees generic review/safety notices (`telegram_nutrition_onboarding_runtime_notice.py:60-73`).

Use:

> 입력 확인이 저장됐어요. 운영자가 내용을 검토합니다.
> 검토가 끝나면 이 토픽에서 안내할게요. 지금은 추가 입력이 필요하지 않습니다.

For safety hold:

> 건강 관련 답변을 확인하기 위해 코칭을 잠시 보류했습니다.
> 증상이나 우려가 있으면 식단 조정보다 의료진 상담을 먼저 받아 주세요.

## Korean replacement copy — onboarding

The field order remains unchanged.

| Field | Replacement |
|---|---|
| `date_of_birth` | `생년월일을 입력해 주세요. 형식: YYYY-MM-DD (예: 1996-08-01)` |
| `equation_sex_basis` | `열량 계산에 사용할 성별 기준을 선택해 주세요: 남성 / 여성 / 선택 안 함` |
| `height_cm` | `현재 키를 cm 숫자로 입력해 주세요. 예: 180` |
| `weight_kg` | `현재 체중을 kg 숫자로 입력해 주세요. 예: 80` |
| `activity_category` | `평소 활동 수준을 선택해 주세요: 좌식 / 가벼움 / 보통 / 매우 활동적 / 최고 활동` |
| `activity_rationale` | `주간 운동 횟수와 평소 활동을 알려 주세요. 예: 주 3회 근력 운동, 앉아서 일함` |
| `goal_type` | `가장 중요한 목표를 선택해 주세요: 감량 / 유지 / 증량` |
| `target_weight_kg` | `목표 체중을 kg 숫자로 입력해 주세요. 예: 70. 유지라면 없음` |
| `target_date` | `목표 날짜를 YYYY-MM-DD로 입력해 주세요. 예: 2026-12-31. 유지라면 없음` |
| `allergies` | `음식 알레르기를 쉼표로 입력해 주세요. 예: 땅콩, 새우. 없으면 없음` |
| `intolerances` | `음식 불내증을 쉼표로 입력해 주세요. 예: 유당, 밀. 없으면 없음` |
| `religious_ethical_exclusions` | `종교·윤리상 먹지 않는 식품을 입력해 주세요. 예: 돼지고기. 없으면 없음` |
| `disliked_foods` | `싫어서 피하는 식품을 입력해 주세요. 예: 고수, 오이. 없으면 없음` |
| `dietary_preferences` | `선호하는 식품이나 식단을 입력해 주세요. 예: 채식, 고단백. 없으면 없음` |
| `conditions` | `진단받은 질환이나 건강상 주의할 점을 입력해 주세요. 없으면 없음` |
| `medications` | `현재 복용 중인 약이나 보충제를 입력해 주세요. 없으면 없음` |
| `pregnancy_breastfeeding` | `현재 임신 중이거나 수유 중인가요? 예 / 아니오 / 확실하지 않음` |
| `eating_disorder_risk` | `섭식장애 위험이나 과거력이 있나요? 예 / 아니오 / 확실하지 않음` |
| `cooking_access` | `사용 가능한 조리 환경을 알려 주세요. 예: 가스레인지와 전자레인지 사용 가능` |
| `budget_band` | `하루 식비 수준을 알려 주세요. 예: 보통 또는 1인 기준 금액` |
| `meal_count` | `평소 하루 몇 끼 드시나요? 2~6 중 숫자로 입력해 주세요. 예: 3` |
| `schedule_constraints` | `식사 시간 제약이 있나요? 예: 12~13시 점심, 아침 거름. 없으면 없음` |

## Korean replacement copy — daily check-in

The 13-step order is unchanged (`physique_checkin.py:747-761`).

| Step | Replacement |
|---|---|
| `bodyweight` | `오늘 아침, 기상 직후 체중을 kg 숫자로 입력해 주세요. 예: 70.2` |
| `calories` | `어제 먹은 총열량을 kcal 숫자로 입력해 주세요. 예: 2350` |
| `macros` | `어제 탄수화물·단백질·지방을 g 단위로 입력해 주세요. 순서: 탄수화물 단백질 지방. 예: 280 150 65` |
| `meals` | `어제 먹은 식사를 끼니별로 편하게 적어 주세요. 간식과 계획에서 벗어난 식사도 포함해 주세요. 예: 아침 오트밀 / 점심 닭가슴살·밥 / 간식 라떼` |
| `water` | `어제 마신 물의 총량을 L 숫자로 입력해 주세요. 예: 2.5` |
| `sleep_duration` | `어제 실제로 잔 시간을 시간 숫자로 입력해 주세요. 예: 7.5` |
| `sleep_quality` | `어제 수면의 질을 선택해 주세요: 1 매우 나쁨 · 2 나쁨 · 3 보통 · 4 좋음 · 5 매우 좋음` |
| `digestion` | `어제 배변·소화 상태를 선택해 주세요. 불편했다면 가장 가까운 항목을 골라 주세요.` |
| `condition` | `오늘 컨디션을 선택해 주세요: 1 매우 나쁨 · 2 나쁨 · 3 보통 · 4 좋음 · 5 매우 좋음` |
| `appetite_stress` | `어제 하루 평균 식욕과 스트레스를 각각 1~5로 입력해 주세요. 예: 식욕 3, 스트레스 2` |
| `training_summary` | `어제 실제로 한 운동이 있다면 부위·종목·시간을 적어 주세요. 없으면 휴식이라고 입력해 주세요.` |
| `optional_note` | `어제 기록에 덧붙일 내용이 있나요? 없으면 없음` |
| `summary` | `오늘 아침 체중과 어제 영양·회복 기록을 확인해 주세요. 맞으면 저장, 고칠 항목은 수정, 나중에 하려면 임시저장을 눌러 주세요.` |

## Conditional daily follow-ups

These can be inserted after sleep quality, condition, or training (`wizard.py:193-196`).

| Follow-up | Replacement |
|---|---|
| `Q-SLEEP-CAUSE` | `수면이 짧거나 질이 낮았다면 가장 가까운 이유를 선택해 주세요.` |
| `Q-SLEEP-ADJUST` | `오늘 수면을 위해 바꿔 볼 수 있는 행동이 있나요? 예 / 아니오` |
| `Q-COND-SYMPTOM` | `오늘 컨디션이 낮다면 가장 가까운 상태를 선택해 주세요.` |
| `Q-COND-INTENSITY` | `오늘 훈련 강도를 어떻게 조정할까요? 유지 / 하향 / 휴식` |
| `Q-PERF-REASON` | `운동 수행이 계획보다 낮았다면 가장 가까운 이유를 선택해 주세요.` |
| `Q-PERF-NEXT` | `다음 운동에서 조정을 적용할까요? 예 / 아니오` |

Existing choice values should remain unchanged (`physique_checkin.py:52-58`).

## Recommended input/button simplifications

- Use one-tap keyboards for onboarding closed choices: sex basis, activity, goal, pregnancy, eating-risk, and meal count.
- For all structured “none” fields, use a button that sends the existing exact text `없음`.
- For the four optional onboarding fields, use one `건너뛰기` button sending the already-supported literal.
- Keep macros as one input string to preserve the existing `macros` field contract; improve only its placeholder.
- Remove the forced English `Meal 1 / Meal 2` format. The field is stored as raw text (`wizard.py:1111-1115`), so natural Korean meal descriptions are sufficient.
- Add `정상` to digestion, mapped to existing `normal`; keep Bristol detail options for safety-relevant specificity.
- Use compact labeled 1–5 buttons for all scale questions; callback values remain `a1`–`a5`.
- Expose an explicit `오늘은 측정 못함` option for weight only if it maps to the existing empty/`None` representation (`wizard.py:1036-1037`, `:1145-1148`). Never substitute `0` or a guessed weight.
- Remove the duplicate daily optional-note choices (`없음` and navigation `건너뛰기`); show one action only.
- Standardize all daily entry labels to `오늘 체크인`; retain `오늘 체크인 수정`.
- Keep `예 / 아니오 / 확실하지 않음` for sensitive safety questions. Do not default uncertainty to `아니오`.

Coverage complete: all 22 onboarding fields, 13 regular daily fields, six conditional daily follow-ups, consent transition, attestation, deferral, summary, and saved-state messaging reviewed.