# Telegram UX Specification (Hajibot)

## Purpose
End-to-end UX and messaging spec for Telegram interactions. Defines onboarding/dialog flows, message templates (FA), inline keyboards, callback schema, deep-links, error handling, and state mapping.

## Principles
- Tone: رک، کوتاه، عمل‌محور. بدون جملات پیچیده یا طولانی.
- Messages: حداکثر 4–6 خط؛ بولت‌ها؛ CTA شفاف. (استثناء: پاسخ‌های قیمت‌یار و تحلیل قیمت صرفاً تحلیلی هستند و CTA یا دعوت به تماس ندارند.)
- Buttons: 2–4 دکمه در هر پیام؛ متن کوتاه + ایموجی. Callback data سبک.
- Idempotent: هر اقدام قابل تکرار؛ ایمن در برابر دوبار لمس.

## State Model
- User state is implicit via DB (users, chat_threads, events). Avoid server-side sessions.
- Use `chat_threads` per module for memory (`previous_response_id`).
- Onboarding fields saved in `users` (role, city_id, notif_level, budget_range?).

## Callback Data Schema
- Format: `HJI:<action>:<payload>` (≤ 64 bytes)
- Examples:
  - `HJI:MENU:MAIN`
  - `HJI:ONB:ROLE|dealer`
  - `HJI:ONB:CITY|<city_id>`
  - `HJI:ONB:BUDGET|low|200-400`
  - `HJI:ONB:NOTIF|normal`
  - `HJI:ADS:NEXT|p=2`
  - `HJI:ADS:SAVE|src=external|bama|code=C123`
  - `HJI:PRICE:START`
  - `HJI:MENU:AFFORDABLE_ADS`
  - `HJI:MENU:AFFORDABLE_ADS_VIEW`
  - `HJI:MENU:AFFORDABLE_ADS_SEARCH`
  - `HJI:MENU:AFFORDABLE_ADS_CAR|<brand>`

## Deep Links
- Start: `https://t.me/<botname>?start=HJI_START`
- Campaigns: `?start=HJI_CAMPAIGN_<key>` (log attribution in `events`)

## Onboarding Flow
1) /start
- Message (FA):
  - «سلام! من حاجی‌بات هستم؛ کمک‌یار خرید و فروش خودرو.»
  - «اول نقش‌ت چیه؟»
- Buttons: [نمایشگاه‌دار], [دلال], [تازه‌کار]
- Callback: `HJI:ONB:ROLE|dealer|showroom|novice`
- **Current implementation:** buttons work; اگر کاربر متنی مثل «دلال» بفرستد، fallback همان مقدار را map می‌کند.

2) City selection
- Message: «شهرت کجاست؟ اسم شهر را بنویس یا از لیست انتخاب کن.»
- Inline search (type-ahead در برنامه) برنامه‌ریزی شده است.
- Buttons (top 5 matches): هر کدام → `HJI:ONB:CITY|<id>`
- **Current implementation:** فعلاً فقط ورودی متنی پشتیبانی می‌شود (بدون لیست پیشنهادی)؛ شهر از `cities` یا `city_aliases` match می‌شود.

3) Budget range
- Message: «بازه بودجه؟»
- Buttons: [۲۰۰–۴۰۰], [۴۰۰–۷۰۰], [۷۰۰+] → `HJI:ONB:BUDGET|low|200-400` …
- **Current implementation:** ورودی متنی (اعداد فارسی/انگلیسی) هم به یکی از بازه‌ها map می‌شود.

4) Notification frequency
- Message: «فرکانس پیام‌ها؟»
- Buttons: [کم], [معمولی], [زیاد] → `HJI:ONB:NOTIF|low|normal|high`
- **Current implementation:** دکمه‌ها آماده‌اند؛ متنی مثل «کم» یا «زیاد» نیز map می‌شود.

5) Finish
- Message: «تمام! منوی اصلی آماده است.»
- Buttons: [فرصت‌های امروز], [آگهی‌های به‌قیمت], [قیمت‌یار], [پشتیبانی]
- Callback: `HJI:MENU:MAIN`
- Reply Keyboard: یک ReplyKeyboard پایدار پایین صفحه نیز نمایش داده می‌شود (Persistent)؛ دکمه «🏠 منوی اصلی» و معادل‌های متنی («منوی اصلی», «منو», /menu, menu) رفتارشان دقیقاً مثل `/start` است (همان متد اجرا می‌شود)؛ ورودی‌های دارای اموجی با نرمال‌سازی اموجی هم پشتیبانی می‌شوند.
- **Current implementation:** فقط پیام متنی ثابت + منوی سخت‌کد شده ارسال می‌شود؛ منو از DB قابل تغییر نیست.

6) Phone Verification (optional, admin-toggle)
- If `require_phone_verification=true` (Settings), immediately after onboarding show:
  - Message: «برای تکمیل ثبت‌نام، شماره موبایل‌ت را بفرست (با دکمه زیر).»
  - ReplyKeyboard (one-time): [📱 ارسال شماره موبایل من] with `request_contact=true` (not inline)
  - On receiving contact: validate that `contact.user_id == from.id`; extract phone in E.164; store in `users.phone` (or `users.phone_e164` as per schema) and emit `PhoneSubmitted` event.
  - If user refuses: allow limited read-only features (configurable) or keep gating (specify policy in Settings).

7) Channel Membership Gate (optional, admin-toggle)
- If `require_channel_membership=true` (Settings), after phone (or after onboarding if phone disabled):
  - Message: «عضویت در کانال‌های زیر ضروری است؛ سپس دکمه «عضو شدم» را بزن.»
  - Buttons: up to N channels from Settings (title + join link), and [✅ عضو شدم]
  - On tap [عضو شدم]: verify membership for each `chat_id` via Telegram `getChatMember` (bot must be admin in channels or have enough rights). If all `status != left/kicked` → pass and emit `MembershipGatePassed`; else show reminder.
  - Admin setting controls number/links/chat_ids and enable/disable the gate.

## Main Menu Cards
- Opportunities (push): «۳ فرصت امروز در تهران. CTR ↑ با ۲ دلیل کوتاه.» Buttons: [⭐ ذخیره], [💰 قیمت‌یار], [🗣️ تمرین مذاکره]
- Affordable Ads: «آگهی‌های به قیمت - چه کاری می‌خواهید انجام دهید؟» Buttons: [👀 مشاهده آگهی‌های فعلی], [🔍 پیدا کردن آگهی جدید]
- Valuation: «قیمت‌یار: مشخصات خودرو را بفرست (برند/مدل، سال، کارکرد، شهر، بدنه).» Buttons: [نمونه] → پاسخ متنی شامل بازه، لنگر، اعتماد و دلایل کوتاه است و به‌صورت سیاستی CTA ندارد.
- Support: «تیکت جدید بساز یا وضعیت قبلی را ببین.» Buttons: [تیکت جدید], [تیکت‌های من]
- **Current implementation:**
  - فرصت‌های امروز: کار می‌کند (نمایش دیتا + ذخیره).
  - آگهی‌های به‌قیمت: کار می‌کند (منوی دوگزینه‌ای + انتخاب خودرو + جستجو).
  - سایر کارت‌ها: فقط متن «در حال توسعه است» یا پیام راهنما بر می‌گردانند؛ صفحه‌بندی/لیست پیاده‌سازی نشده است.

## Affordable Ads Flow (آگهی‌های به‌قیمت)

### 1) Main Affordable Ads Menu
- **Trigger**: Main menu button "💸 آگهی‌های به‌قیمت" → `HJI:MENU:AFFORDABLE_ADS`
- **Message**: «🚗 آگهی‌های به قیمت\n\nچه کاری می‌خواهید انجام دهید؟»
- **Buttons**: 
  - [👀 مشاهده آگهی‌های فعلی] → `HJI:MENU:AFFORDABLE_ADS_VIEW`
  - [🔍 پیدا کردن آگهی جدید] → `HJI:MENU:AFFORDABLE_ADS_SEARCH`
  - [🔙 بازگشت به منو] → `HJI:MENU:ROOT`

### 2A) View Current Ads (`AFFORDABLE_ADS_VIEW`)
- **Message**: «📋 آگهی‌های فعلی:\n\n📊 تعداد آگهی‌ها: X\n\n[لیست آگهی‌ها]»
- **Content**: Up to 10 opportunities with status='reviewed' and score ≥ 0.6
- **Format per item**: «{brand} {year} | {transmission} ({year})\n💰 قیمت: {price} میلیون تومان\n⭐ امتیاز: {score}\n🔗 لینک: {url}»
- **Buttons**: 
  - [🔍 پیدا کردن آگهی جدید] → `HJI:MENU:AFFORDABLE_ADS_SEARCH`
  - [🔙 بازگشت] → `HJI:MENU:AFFORDABLE_ADS_MENU`
- **Empty state**: «فعلاً آگهی به‌قیمت مناسبی پیدا نکردم. بعداً دوباره سر بزن!»

### 2B) Search New Ads (`AFFORDABLE_ADS_SEARCH`)
- **Message**: «🚗 انتخاب خودرو برای جستجو:\n\nلطفاً خودرو مورد نظر را انتخاب کنید:»
- **Buttons**: Car selection grid (2x4)
  - [پژو 206] → `HJI:MENU:AFFORDABLE_ADS_CAR|206`
  - [ساینا] → `HJI:MENU:AFFORDABLE_ADS_CAR|ساینا`
  - [کوییک] → `HJI:MENU:AFFORDABLE_ADS_CAR|کوییک`
  - [تیبا] → `HJI:MENU:AFFORDABLE_ADS_CAR|تیبا`
  - [تیبا 2] → `HJI:MENU:AFFORDABLE_ADS_CAR|تیبا 2`
  - [پژو 207] → `HJI:MENU:AFFORDABLE_ADS_CAR|207`
  - [پراید] → `HJI:MENU:AFFORDABLE_ADS_CAR|پراید`
  - [🔙 بازگشت] → `HJI:MENU:AFFORDABLE_ADS_MENU`

### 3) Car Search Processing (`AFFORDABLE_ADS_CAR|<brand>`)
- **Progress Message**: «🚗 شما {brand} را انتخاب کردید\n\n🔍 در حال جستجو و تحلیل آگهی‌های این خودرو...\n\n⏳ لطفاً صبر کنید...»
- **Processing**: Executes `php artisan ads:scrape-cars {brand} --force` via Artisan::call()
- **Filters Applied**:
  - **Time**: Last 7 days (`created_at >= now()->subDays(7)`)
  - **Location**: User's city (`city_id = $user->city_id`)
  - **Quality**: Score ≥ 0.6 and status = 'reviewed'
  - **Brand**: Matches in `brand`, `model`, or mapped brand fields

### 4A) Search Success Results
- **Message**: «✅ آگهی‌های {brand} (7 روز گذشته - {city_name}):\n\n📊 تعداد آگهی‌ها: X\n\n[لیست نتایج]»
- **Content**: Same format as view current ads
- **Buttons**: 
  - [🔍 جستجوی خودرو دیگر] → `HJI:MENU:AFFORDABLE_ADS_SEARCH`
  - [🔙 بازگشت] → `HJI:MENU:AFFORDABLE_ADS_MENU`

### 4B) Search No Results
- **Message**: «⚠️ هیچ آگهی‌ای برای این خودرو یافت نشد\n\n💡 این ممکن است به دلیل:\n• آگهی‌های 7 روز گذشته محدود\n• فیلتر شهر ({city_name})\n• امتیاز پایین (زیر 0.6)\n• وضعیت مرور نشده\n\n🔍 می‌توانید خودرو دیگری را انتخاب کنید:»
- **Buttons**: Back to car selection menu

### Data Constraints & Limitations
- **7-Day Filter**: Only opportunities created within last 7 days
- **City Filter**: Only matches user's city_id (if set during onboarding)
- **Score Filter**: Only opportunities with score ≥ 0.6
- **Status Filter**: Only opportunities with status = 'reviewed'
- **Brand Mapping**: Uses getBrandMapping() for search terms
- **No Pagination**: Limited to reasonable result count
- **Fresh Scrape Issue**: May return no results immediately after scraping due to review/scoring timing

## Error & Unknown Handling
- Rate-limit: «درخواست زیاد بود؛ چند ثانیه صبر کن 🙏»
- Invalid input: «متوجه نشدم؛ لطفاً دقیق‌تر بفرست 🌱»
- Feature blocked (quota): «به سقف پلن رسیدی؛ برای ادامه ارتقا بده 💳» [پرداخت]
- Phone missing (gate enabled): «برای ادامه، شماره موبایل‌ت را با دکمه زیر بفرست.» [📱 ارسال شماره موبایل]
- Membership missing (gate enabled): «برای ادامه، ابتدا عضو کانال‌های معرفی‌شده شو و سپس دکمه «عضو شدم» را بزن.» [✅ عضو شدم]
- AI ambiguous: «برای قیمت‌یار، اطلاعات کامل‌تر بدهید (سال/کارکرد/بدنه/شهر).»

## Save Deal / CTA Clicks
- For each card: inline buttons include a unique push_log id in callback, or link با پارامتر رهگیری.
- Click handler:
  - Marks `push_logs.clicked_cta_at = now()`
  - Emits `SaveDeal` or `ViewDeal` as appropriate.
- **Current implementation:** «⭐ ذخیره» با `source=opportunity`، دکمه‌های قیمت‌یار (درخواست تحلیل) و پیام‌های تحلیلی بدون CTA تماس برمی‌گردند؛ سایر CTAها و push_logs هنوز مقداردهی نمی‌شوند.

## City Search Logic
- When user types a city name (FA/FA-EN), search `cities` and `city_aliases` normalized.
- Top N matches (≤ 5) as buttons; fallback: manual list of provinces→cities.

## Upsell Paths
- When FeatureGuard denies:
  - Reply: «به سقف پلن رسیدی؛ ارتقا بده تا ادامه دهیم.» Buttons: [ارتقا به عضو], [پلن‌ها]
  - Event: `UpgradeClick`

## Quick Visit (Inspection) UX (Outline)
- Ask 8–10 quick questions; accumulate risk bullets; return risk-map + next steps.
- Buttons: [ذخیره], [ارسال به همکار], [چک‌لیست کامل]

## Negotiation UX (Outline)
- Two modes: [سناریو آماده] [تمرین واقعی]
- Scenario starters: مودب/قاطع/حقوقی (sample scripts)
- Guided Q&A: ask for context; propose 3-step plan; track turns in `chat_threads`.

## Analytics Hooks
- Start, SelectCity, ViewDeal, SaveDeal, CallFromCard, PriceCheck, ChecklistRun, PushOpen, UpgradeClick, UpgradeSuccess.
- **Current implementation:** فقط `Start`, `SelectCity`, `ViewDeal`, `SaveDeal`, `UpgradeClick`, `UpgradeSuccess` ثبت می‌شوند؛ سایر رخدادها هنوز اضافه نشده‌اند.
