Files
tehran_taj/docs/EXPERT-ACT-AS-PLAN.md
T

189 lines
13 KiB
Markdown
Raw Normal View History

# طرح — بازگرداندن «ورود کارشناس به حساب مشتری» (act-as)
تاریخ: ۱۴۰۵/۰۷/۲۰ (2026-10-12)
## وضعیت: ✅ **انجام شد** — تصمیم مالک ۲۰۲۶/۱۰/۱۲
> «نه دقیقا همون چیزی که بود در ساختار مرجع همون رو بیار ولی با ساختار درستی که
> خودت میدونی»
یعنی: **دامنه کامل مرجع**، با سازوکار امنِ بند ۳. پس **بند ۴ (مرزها) حذف شد** —
هیچ مسیری در حالت act-as بسته نیست، آپلود مدارک و logout هم باز است، چون مرجع
هم interceptor سراسری داشت و همه‌چیز از طرف مشتری می‌رفت.
بقیه طرح عیناً اجرا شد. سه چیز که در طرح نبود و در حین کار پیدا شد (هر سه رفع
شد): پاک‌سازی `queryCache`/علاقه‌مندی/اجرت کش‌شده در جابجایی، و هدر در مسیر
آپلود که `request()` را دور می‌زند. جزئیات: `ISSUES.md` EXP-01/02/03 و D29.
---
### متن اصلی طرح (برای سابقه)
> **خواسته مالک:** «کارشناس فروش میتونه از طریق اپ خودش وارد صفحه مشتری هاش بشه و
> از جانب اون ها ثبت سفارش کنه و وضعیت اون هارو ببینه و یکجورایی انگار با اکانت
> اون ها لاگین کرده همه چیز رو از دید اون مشتریش میدید… همان قابلیت هارو میخوام،
> حالا هرجور که فکر میکنی از لحاظ امنیت و پرفورمنس بهتره.»
**همان قابلیت، بدون سه عیبِ روش قدیم.** تصمیم‌های فنی زیر با من است؛ هرجا چیزی
از مرجع **کم** شده باشد، صریح نوشته‌ام.
---
## ۱. مرجع چه می‌کرد (بررسی‌شده، نه حدس)
| مرحله | کد مرجع |
|---|---|
| لیست مشتریان | `POST /my-child` → `ApiMobileController.php:185-196`، فیلتر `reagent_id = auth()->id()` |
| **توکن مشتری ساخته می‌شد** | `:197-201` — اگر `api_token` مشتری خالی بود، `Str::random(60)` می‌ساخت و **ذخیره** می‌کرد |
| کلیک روی کارت | `customerChoice.vue:155` → `changeUser()` → `myStore.toggleActiveUser(user)` |
| ذخیره توکن | `myDataStore.ts:163` → `localStorage.setItem('activeChildToken', value.api_token)` |
| اعمال روی همه درخواست‌ها | `tokenManage.ts:8` `getUserToken()` = توکن مشتری **یا** توکن خودش → `plugins/axios.ts:18` روی **هر** درخواست |
| خروج از حساب مشتری | `Dashboard.vue:784` → پاک کردن `activeChildToken` |
یعنی دامنه مرجع: **هر کاری که مشتری می‌توانست**، چون interceptor سراسری بود.
## ۲. سه عیبی که تکرار نمی‌کنم
۱. **توکن دائمی مشتری در دست کارشناس.** `api_token` انقضا نداشت و در
`localStorage` می‌ماند. کارشناسی که فردا قطع همکاری می‌کرد، توکن مشتریانش را
داشت و همچنان کار می‌کرد.
۲. **هیچ ردی نمی‌ماند.** درخواست عیناً از طرف مشتری می‌آمد، پس در سرور معلوم
نبود این سفارش را مشتری ثبت کرده یا کارشناس. برای مالی، این یعنی غیرقابل
حسابرسی.
۳. **بررسی مالکیت فقط یک‌بار.** توکن در لحظه گرفتن صادر می‌شد؛ بعد از آن اگر
مشتری به کارشناس دیگری منتقل می‌شد، توکن قدیمی همچنان کار می‌کرد.
## ۳. طراحی پیشنهادی
**هسته: توکنِ مشتری صادر نمی‌شود. کارشناس با توکن خودش درخواست می‌دهد و
می‌گوید «از طرف مشتری X».**
### چرا این انتخاب (و نه گزینه‌های دیگر)
`CustomerJwtStrategy.validate()` در تارگت همیشه یک `AuthenticatedCustomer`
می‌سازد که `id` آن **یک Customer id** است، و **هر ۱۷ مسیر مشتری** از همان
استفاده می‌کنند. پس اگر جابجایی را همان‌جا اعمال کنم، همه آن مسیرها **بدون یک خط
تغییر** از دید مشتری کار می‌کنند — همان «همه‌چیز را از دید او می‌بیند» که خواستی،
با کمترین سطح تماس با کد.
| گزینه | نتیجه | رأی |
|---|---|---|
| صدور توکن مشتری (روش مرجع) | ساده، ولی هر سه عیب بالا | ❌ |
| پارامتر `customerId` روی هر مسیر | باید ۱۷ مسیر و همه سرویس‌ها عوض شوند؛ فراموش کردن یکی = حفره | ❌ |
| **هدر `X-Act-As-Customer` + اعمال در strategy** | یک نقطه تغییر، هر ۱۷ مسیر خودکار، قابل حسابرسی | ✅ |
### سمت سرور
**۱. هدر درخواست.** کارشناس توکن خودش را می‌فرستد به‌علاوه
`X-Act-As-Customer: <customerId>`.
**۲. اعمال در `validateExpert`** (تنها نقطه تغییر منطق دسترسی):
```
اگر هدر بود و principal کارشناس بود:
مشتری را بخوان: id = هدر، referredByAgentId = admin.id، isActive، !isArchived
اگر نبود → 403 (مشتری دیگری، یا مشتریِ کارشناس دیگر)
اگر بود → { id: customer.id, ← از دید مشتری
actingAsCustomerId: customer.id,
adminUserId: admin.id, ← هویت واقعی حفظ می‌شود
type: 'sales-expert' }
```
نکات مهم این طراحی:
- **بررسی مالکیت در هر درخواست** انجام می‌شود، نه یک‌بار. مشتری که به کارشناس
دیگری منتقل شود، دسترسی **در همان لحظه** قطع می‌شود. (عیب ۳ رفع شد.)
- کارشناس غیرفعال/تنزل‌یافته همان‌جا ۴۰۱ می‌گیرد، چون `validateExpert` از قبل
`isActive` و `role` را هر بار می‌خواند. (عیب ۱ رفع شد.)
- `adminUserId` حفظ می‌شود ⇒ هویت واقعی هرگز گم نمی‌شود. (عیب ۲ رفع شد.)
- توکن **عوض نمی‌شود**؛ پس چیزی در `localStorage` نمی‌نشیند که بعداً سوءاستفاده
شود. بستن اپ = پایان حالت act-as.
**۳. هزینه پرفورمنس (خواسته‌ات).** یک `findUnique` اضافه روی `Customer` با
`where: { id }` — کلید اصلی، پس index-only. **فقط** وقتی هدر وجود دارد؛ یعنی
درخواست‌های مشتری عادی و کارشناس در حالت معمولی **صفر هزینه اضافه** دارند. در
حالت act-as هم جایگزین کوئریِ `tradingCustomer` می‌شود، نه اضافه بر آن — پس
تعداد کوئری ثابت می‌ماند.
**۴. ⛔ این بند اجرا نشد.** مالک دامنه کامل مرجع را خواست، پس هیچ‌کدام از این
مرزها گذاشته نشد — همه مسیرها در حالت act-as باز است:
| مسیر | در حالت act-as | چرا |
|---|---|---|
| `POST /orders` | ✅ مجاز | خواسته اصلی |
| مشاهده: `me`، `me/orders`، `me/ledger-entries`، `me/report*`، `accounting-balance`، `wage-quote` | ✅ مجاز | «وضعیت آن‌ها را ببیند» |
| علاقه‌مندی‌ها | ✅ مجاز | بی‌خطر، و بخشی از «از دید او» |
| `POST /customer/me/documents` و `complete-profile` | ❌ **۴۰۳** | آپلود مدارک هویتی به‌جای مشتری. کارشناس مدرکِ مشتری را ندارد و این کار جعل هویت است، نه خدمت. در پنل ادمین انجام می‌شود. |
| `POST /auth/logout` | ❌ **۴۰۳** | نشست مشتری را نباید کارشناس ببندد |
| مسیرهای `expert/*` | ❌ **۴۰۳** | در حالت act-as بی‌معناست؛ از ابهام جلوگیری می‌کند |
**۵. ثبت در سفارش.** `OrdersService.createOrder` از قبل `placedBy` دارد. سفارشِ
act-as با `placedBy: 'operator'` ثبت می‌شود — یعنی قیمت را سرور تحمیل نمی‌کند و
عدد کارشناس می‌ماند؟ **نه**: برای این حالت `placedBy: 'customer'` می‌گذارم تا
قیمت‌گذاری سرور (MON-08) اعمال شود، چون این سفارشِ مشتری است نه اپراتورِ پنل.
علاوه بر آن `salesAgentId` از قبل خودکار از `customer.referredByAgentId` پر
می‌شود — که همان کارشناس است. **ردّ حسابرسی** هم در لاگ ثبت می‌شود:
`expert <adminUserId> acting as customer <id>`.
**۶. پیامک.** مشتری پیامک «سفارش شما ثبت شد» را **می‌گیرد** (مثل مرجع). دلیل:
روی حساب و بدهی طلای او نشسته و باید بداند. کارشناس هم پیامک ارجاع‌دهنده را
می‌گیرد، چون `referredByAgentId` خودش است — رفتار موجود، تغییری لازم نیست.
### سمت اپ (هر دو اپ)
**۱. وضعیت act-as** در `sessionStore` (نه `localStorage`): `actingAs = {id, name}`.
چون در `sessionStorage`/حافظه است، بستن تب پایانش می‌دهد — برعکس مرجع.
**۲. کلاینت API**: اگر `actingAs` ست بود، هدر `X-Act-As-Customer` به همه
درخواست‌ها اضافه شود (یک نقطه، در `request()` کلاینت).
**۳. `CustomerChoiceView`**: زدن روی کارت ⇒ `changeUser` ⇒ ست کردن `actingAs` و
رفتن به خانه، **عیناً مثل مرجع** (`customerChoice.vue:155-167`).
**۴. ⚠️ نوار هشدار همیشگی** — چیزی که مرجع نداشت و از نظر من ضروری است: یک نوار
ثابت بالای صفحه «در حال مشاهده به‌عنوان **فلانی** — خروج». بدون این، کارشناس
نمی‌فهمد در حساب کیست و ممکن است سفارش را روی حساب اشتباهی ثبت کند. این ریسک
واقعیِ روش مرجع بود.
**۵. پاک کردن سبد در جابجایی.** سبد در `localStorage` است؛ اگر پاک نشود، کالاهای
سبدِ کارشناس وارد سفارش مشتری می‌شود. در جابجایی (ورود و خروج) سبد خالی می‌شود.
## ۴. تست‌ها
| تست | می‌سنجد |
|---|---|
| کارشناس + هدر مشتریِ خودش | داده‌های آن مشتری برمی‌گردد |
| کارشناس + هدر **مشتریِ کارشناس دیگر** | ۴۰۳ |
| کارشناس + هدر مشتری غیرفعال/آرشیو | ۴۰۳ |
| **مشتری عادی** + هدر | هدر **نادیده** گرفته می‌شود (نه ۴۰۳ خاموش؛ اصلاً اثر ندارد) |
| کارشناس غیرفعال‌شده + هدر معتبر | ۴۰۱ فوری |
| سفارش در حالت act-as | `customerId` = مشتری، `salesAgentId` = کارشناس، قیمت از سرور |
| مسیرهای ممنوع (مدارک، logout) | ۴۰۳ |
| بدون هدر | رفتار امروز، بدون تغییر (regression) |
## ۵. چه چیزی عمداً مثل مرجع نیست
| مرجع | اینجا | دلیل |
|---|---|---|
| توکن دائمی مشتری به کارشناس داده می‌شد | هیچ توکنی صادر نمی‌شود | سرقت نشست؛ بعد از قطع همکاری هم کار می‌کرد |
| `api_token` در `localStorage` | وضعیت در حافظه نشست | بستن اپ = پایان |
| مالکیت یک‌بار بررسی می‌شد | هر درخواست | انتقال مشتری فوراً اثر می‌کند |
| ~~کارشناس همه‌کاره بود~~ | ~~مدارک/logout ممنوع~~ | **اجرا نشد** — مالک دامنه کامل مرجع را خواست؛ کارشناس همه‌کاره است |
| هیچ ردی نبود | لاگ + `salesAgentId` | حسابرسی مالی |
| هیچ نشانه‌ای در UI نبود | نوار همیشگی | سفارش روی حساب اشتباه |
## ۶. برآورد
| بخش | کار |
|---|---|
| سرور | `app-principal.ts` (+`actingAsCustomerId`)، `customer-jwt.strategy.ts` (منطق)، یک گارد برای مسیرهای ممنوع، لاگ در `orders.service` |
| اپ‌ها (×۲) | `sessionStore`، `api/client.ts` (هدر)، `CustomerChoiceView` (changeUser)، نوار act-as، پاک کردن سبد |
| تست | ۸ مورد بالا در `apps/api` |
ترتیب اجرا: سرور + تست‌ها اول (قابل اثبات)، بعد اپ‌ها.
---
**منتظر تأیید.** اگر با مرزهای بند ۴ (ممنوع بودن آپلود مدارک و logout) موافق
نیستی بگو — بقیه طرح تصمیم فنی است و خودم جلو می‌برم.