# طرح — بازگرداندن «ورود کارشناس به حساب مشتری» (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: `. **۲. اعمال در `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 acting as customer `. **۶. پیامک.** مشتری پیامک «سفارش شما ثبت شد» را **می‌گیرد** (مثل مرجع). دلیل: روی حساب و بدهی طلای او نشسته و باید بداند. کارشناس هم پیامک ارجاع‌دهنده را می‌گیرد، چون `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) موافق نیستی بگو — بقیه طرح تصمیم فنی است و خودم جلو می‌برم.