feat(search): enhance user search functionality to support Persian digits and user IDs
This commit is contained in:
@@ -306,7 +306,7 @@ export default function UsersPage() {
|
|||||||
<input
|
<input
|
||||||
value={searchInput}
|
value={searchInput}
|
||||||
onChange={(e) => setSearchInput(e.target.value)}
|
onChange={(e) => setSearchInput(e.target.value)}
|
||||||
placeholder="نام، موبایل یا ایمیل..."
|
placeholder="شماره کاربری، نام، موبایل یا ایمیل..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ minWidth: 160 }}>
|
<div style={{ minWidth: 160 }}>
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
# نوبتدهی آنلاین منبعمحور در nobat724
|
||||||
|
|
||||||
|
> وضعیت: **طرح پیشنهادی** — هنوز پیاده نشده. مرجعِ تصمیمگیری پیش از شروع کار.
|
||||||
|
> دامنه: `clinicpro` (بکاند) + `nobat724_front` (سایت عمومی).
|
||||||
|
> پیشنیاز خواندن: [resource-first-model.md](resource-first-model.md) · [docs/api/appointment.md](../api/appointment.md) · [docs/api/resource.md](../api/resource.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۱. صورت مسئله
|
||||||
|
|
||||||
|
نوبتدهی منبعمحور امروز فقط از **پنل** کار میکند. بیماری که وارد سایت میشود، برای
|
||||||
|
کلینیکی که در حالت `resource` است یا چیزی نمیبیند، یا یک تقویمِ اسلاتیِ بیربط.
|
||||||
|
|
||||||
|
سه حالت نوبتدهی در سیستم هست ([WeeklySchedule.php](../../src/Appointment/Entity/WeeklySchedule.php)):
|
||||||
|
|
||||||
|
| حالت | یعنی | سایت امروز؟ |
|
||||||
|
|---|---|---|
|
||||||
|
| `slot` | شبکهٔ اسلات ثابت پزشک | ✅ تاریخ → ساعت |
|
||||||
|
| `service` | طول نوبت از مدت سرویسها | ✅ سرویس → تاریخ → ساعت |
|
||||||
|
| `resource` | برنامهٔ چندبخشی روی **تقویم منابع** | ❌ هیچ |
|
||||||
|
|
||||||
|
`booking_mode` برای هر محل جدا میآید (`GET /api/v1/appointment-booking-locations/{doctorUuid}`)
|
||||||
|
و سایت آن را از **محل انتخابشده** میخواند، نه از پزشک — این قرارداد سرِ جایش میماند و
|
||||||
|
`resource` فقط حالت سوم همان سوییچ است.
|
||||||
|
|
||||||
|
### چرا نمیشود همین موتور را به سایت وصل کرد
|
||||||
|
|
||||||
|
موتور منبعمحور (`appointment-availability` → `appointment-hold` → `appointment-confirm`)
|
||||||
|
**ذاتاً پنلی** است، نه فقط «احراز هویت لازم دارد»:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// AvailabilityController::search() و BookingController::create()
|
||||||
|
$address = $this->branches->resolve($user, $data['branch_uuid']); // ← محیطِ خودِ کاربر
|
||||||
|
[$entityType, $entityId] = $this->branches->pair($user); // ← محیطِ خودِ کاربر
|
||||||
|
```
|
||||||
|
|
||||||
|
`AddressResolver::pair()` محیط را از **کاربرِ درخواستدهنده** میگیرد. بیمار هیچ محیطی
|
||||||
|
ندارد، پس همین حالا هم اگر توکنِ بیمار بفرستیم، آدرس کلینیک «یافت نشد» میشود. یعنی
|
||||||
|
مسئله یک خط `security.yaml` نیست؛ **جهتِ resolve** باید برعکس شود: محیط باید از
|
||||||
|
**مقصدِ رزرو** (پزشک/کلینیک) بیاید، نه از فرستنده.
|
||||||
|
|
||||||
|
سه گاردِ دیگر هم که مسیرهای عمومی از قبل دارند، در این موتور **اصلاً وجود ندارند** —
|
||||||
|
چون تا امروز فقط کارمند صدایش میزده:
|
||||||
|
|
||||||
|
| گارد | مسیر عمومیِ اسلاتی/سرویسی | موتور منبعمحور |
|
||||||
|
|---|---|---|
|
||||||
|
| `online_booking_enabled` | ✅ [SlotCalculatorService.php:197](../../src/Appointment/Service/SlotCalculatorService.php#L197) | ❌ |
|
||||||
|
| پنجرهٔ رزرو (`booking_window_*`) | ✅ | ❌ |
|
||||||
|
| «فقط سرویسهای bookable» | ✅ | جزئی |
|
||||||
|
| عدم افشای منابع | — | ❌ پاسخ، نام دستگاه و اپراتور را میدهد |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۲. تصمیم معماری
|
||||||
|
|
||||||
|
سه راه روی میز بود:
|
||||||
|
|
||||||
|
| گزینه | کار | چرا نه / چرا آری |
|
||||||
|
|---|---|---|
|
||||||
|
| **الف. مسیر عمومیِ موازی روی همان موتور** | سه اندپوینت عمومی که محیط را از مقصد resolve میکنند و گاردهای عمومی را دارند | ✅ **پیشنهادی.** موتور تخصیص، ظرفیت و اتمیکبودن یکی میماند؛ فقط لایهٔ ورودی/گارد جدا میشود |
|
||||||
|
| ب. بیمار خودش منبع را انتخاب کند (مثل مودال پنل) | استفادهٔ مستقیم از `resource/{uuid}/service-slots` | ❌ بیمار نباید بین «لیزر CO2 شمارهٔ ۲» و «۳» انتخاب کند؛ این تصمیمِ کلینیک است و افشای ظرفیت داخلی هم هست |
|
||||||
|
| ج. تنزل `resource` به `service` برای سایت | نادیده گرفتن منابع در مسیر عمومی | ❌ نوبتِ ثبتشده منبع نمیگیرد، پس پنل و سایت دو حقیقت متفاوت از ظرفیت میسازند و دابلبوکینگ قطعی است |
|
||||||
|
|
||||||
|
**تصمیم: گزینهٔ الف.** هستهٔ `AvailabilityEngine` / `HoldService` / `BookingService` دست
|
||||||
|
نمیخورد؛ فقط سه کنترلر نازکِ عمومی روی آن مینشیند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۳. قرارداد API پیشنهادی
|
||||||
|
|
||||||
|
همه زیر `/api/v1/public/...` تا از مسیر پنلی جدا بماند و در `security.yaml` یکجا
|
||||||
|
whitelist شود. احراز هویتِ بیمار برای دو مرحلهٔ آخر لازم است (مثل `POST /api/v1/appointment`).
|
||||||
|
|
||||||
|
### ۳.۱ `GET /api/v1/appointment-booking-services/{doctorUuid}` — بدون تغییر
|
||||||
|
|
||||||
|
از قبل عمومی است و `booking_mode` را میدهد. سایت با دیدن `"resource"` جریان تازه را
|
||||||
|
شروع میکند. تنها افزودهٔ لازم: در این حالت هم `services[]` باید سرویسهای **قابل رزرو
|
||||||
|
آنلاین** باشند (`ServiceItem.bookable`).
|
||||||
|
|
||||||
|
### ۳.۲ `GET /api/v1/public/resource-availability/month` — تقویم ماه
|
||||||
|
|
||||||
|
```
|
||||||
|
?doctor_uuid=…&clinic_uuid=…&service_uuid=…&item_uuids[]=…&year=1405&month=5
|
||||||
|
→ { enabled_dates: [...], disabled_dates: [...], online_booking_enabled: true,
|
||||||
|
booking_window: { value: 3, unit: "month" } }
|
||||||
|
```
|
||||||
|
|
||||||
|
عمداً سبک: فقط «این روز ظرفیت دارد یا نه»، بدون ساختِ تخصیص. معادلِ موجودِ پنلیاش
|
||||||
|
`appointment-availability/month` است.
|
||||||
|
|
||||||
|
### ۳.۳ `POST /api/v1/public/resource-availability` — زمانهای یک روز
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "doctor_uuid": "…", "clinic_uuid": "…", "service_uuid": "…",
|
||||||
|
"item_uuids": ["…"], "date": "2026-08-04", "patient_gender": "woman" }
|
||||||
|
```
|
||||||
|
|
||||||
|
پاسخ **بدون** `assignment`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "plan": { "total_minutes": 75, "segments": [ { "name": "لیزر", "duration_minutes": 50 } ] },
|
||||||
|
"slots": [ { "start": 1785220200, "end": 1785224700 } ],
|
||||||
|
"reason": null }
|
||||||
|
```
|
||||||
|
|
||||||
|
> **افشای منابع ممنوع.** پاسخ پنلی `assignment` (نام دستگاه و اپراتور) دارد؛ نسخهٔ
|
||||||
|
> عمومی فقط زمان میدهد. تخصیص سمت سرور در `hold` نگه داشته میشود.
|
||||||
|
|
||||||
|
### ۳.۴ `POST /api/v1/public/resource-hold` — نگهداشتن موقت (احراز شده)
|
||||||
|
|
||||||
|
ورودی: همان کلیدها + `start`. خروجی: `{ hold_uuid, starts_at, ends_at, expires_at }`.
|
||||||
|
|
||||||
|
تخصیص منبع را **سرور** انتخاب میکند (`resource_strategy` محیط: `first_available` /
|
||||||
|
`least_loaded` / …) و بیمار در آن نقشی ندارد. hold اجباری است: بین دیدن وقت و پرداخت،
|
||||||
|
صندلی باید قفل شود وگرنه دو بیمار همزمان یک دستگاه را میخرند.
|
||||||
|
|
||||||
|
### ۳.۵ `POST /api/v1/public/resource-confirm` — ثبت نهایی (احراز شده)
|
||||||
|
|
||||||
|
ورودی `{ hold_uuid, patient_national_code, patient_gender, … }`. خروجی همان
|
||||||
|
`appointment_uuid` + `price_snapshot`. نوبت `pending` متولد میشود و مسیر پرداخت/بیعانهٔ
|
||||||
|
موجود دستنخورده میماند.
|
||||||
|
|
||||||
|
### ۳.۶ گاردهای مشترکِ هر چهار اندپوینت
|
||||||
|
|
||||||
|
1. `online_booking_enabled` برای همان (پزشک، محیط) — خاموش ⇒ `403` صریح، نه فهرست خالی.
|
||||||
|
2. پنجرهٔ رزرو `booking_window_value/unit` — تاریخ خارج از پنجره ⇒ `422`.
|
||||||
|
3. `booking_mode === 'resource'` — در غیر اینصورت `422` با اشاره به مسیر درست.
|
||||||
|
4. سرویس باید `bookable` و مالِ همان محیط باشد.
|
||||||
|
5. محیط از **مقصد** resolve میشود: `EntityContext::forBooking($doctor, $clinic)` — نه از کاربر.
|
||||||
|
6. Rate limit روی `hold` (بیمار میتواند با چند hold ظرفیت را قفل کند).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۴. کلید روشن/خاموش برای مدیر کلینیک
|
||||||
|
|
||||||
|
خواستهٔ صریح: **مدیر کلینیک باید بتواند نوبتدهی آنلاین را داشته باشد یا نه.**
|
||||||
|
این کلید از قبل وجود دارد و لازم نیست چیز تازهای اختراع شود — فقط باید در مسیر
|
||||||
|
منبعمحور هم **خوانده** شود.
|
||||||
|
|
||||||
|
### وضع موجود
|
||||||
|
|
||||||
|
| لایه | کجا |
|
||||||
|
|---|---|
|
||||||
|
| ذخیره | `WeeklySchedule.meta.online_booking_enabled` per (پزشک، محیط) — [WeeklySchedule.php:50](../../src/Appointment/Entity/WeeklySchedule.php#L50) |
|
||||||
|
| کنترل در پنل | سوییچ «نوبتدهی آنلاین» در [ScheduleSection.tsx:886](../../assets/admin/components/schedule/ScheduleSection.tsx#L886) |
|
||||||
|
| اعمال در مسیر اسلاتی/سرویسی | [SlotCalculatorService.php:197](../../src/Appointment/Service/SlotCalculatorService.php#L197) و `:315` (فقط وقتی `forManagement` نباشد) |
|
||||||
|
| اثر روی فهرستها | پزشکِ همهخاموش از فهرست عمومی حذف میشود — [DoctorRepository.php:98](../../src/Doctor/Repository/DoctorRepository.php#L98) |
|
||||||
|
|
||||||
|
نکتهٔ مهم: خاموشبودن آنلاین **هرگز** جلوی ثبت نوبت از پنل را نمیگیرد — منشی همچنان
|
||||||
|
نوبت میدهد. همین رفتار باید در حالت منبعمحور هم عیناً حفظ شود.
|
||||||
|
|
||||||
|
### آنچه باید اضافه شود
|
||||||
|
|
||||||
|
1. **خواندن همان کلید در موتور منبعمحور.** هر چهار اندپوینت عمومی بند ۳ اول این را
|
||||||
|
بسنجند. بدون این، عمومیکردن موتور یعنی کلیدِ خاموشِ کلینیک بیاثر میشود — یعنی
|
||||||
|
نشت رفتار، نه یک نقص کوچک.
|
||||||
|
2. **سه سطحِ کنترل، از درشت به ریز:**
|
||||||
|
|
||||||
|
| سطح | کلید | وضعیت |
|
||||||
|
|---|---|---|
|
||||||
|
| کل محیط/پزشک | `online_booking_enabled` | ✅ هست |
|
||||||
|
| هر سرویس | `ServiceItem.bookable` («نمایش در نوبتدهی») | ✅ هست |
|
||||||
|
| هر منبع | `ClinicResource.online_bookable` | ⛔ **پیشنهاد فاز ۲** |
|
||||||
|
|
||||||
|
سطح سوم برای دستگاهی است که باید در گردش کار داخلی بماند ولی مستقیم آنلاین فروخته
|
||||||
|
نشود. تا وقتی نیست، همان `active` تنها اهرم است — و خاموشکردنش نوبتدهی پنلی را هم
|
||||||
|
میکُشد، که همان چیزی نیست که مدیر میخواهد.
|
||||||
|
3. **پاسخِ «خاموش است» باید صریح باشد.** فهرست خالی، هم بیمار را گیج میکند هم
|
||||||
|
پشتیبانی را. یک `403` با پیام «نوبتدهی آنلاین این کلینیک فعال نیست» + پنهانکردن
|
||||||
|
دکمهٔ رزرو در سایت.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۵. جریان کاربر در سایت
|
||||||
|
|
||||||
|
```
|
||||||
|
انتخاب محل (booking_mode از همان محل)
|
||||||
|
└─ resource ─→ ۱. انتخاب سرویس (یک یا چند) ← appointment-booking-services
|
||||||
|
۲. تقویم ماه ← public/resource-availability/month
|
||||||
|
۳. زمانهای روز ← public/resource-availability
|
||||||
|
۴. نگهداشتن + شمارش معکوس ← public/resource-hold
|
||||||
|
۵. مشخصات بیمار و پرداخت ← public/resource-confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
نگاشت به کد موجود `nobat724_front`:
|
||||||
|
|
||||||
|
| گام | فایل | کار |
|
||||||
|
|---|---|---|
|
||||||
|
| ارکستراسیون | `components/appointment/index.js` | شاخهٔ سوم برای `booking_mode === 'resource'` |
|
||||||
|
| انتخاب سرویس | `components/appointment/service/` | تقریباً بدون تغییر؛ مدت کل از سرور میآید |
|
||||||
|
| تقویم/ساعت | `components/appointment/date/`, `lib/appointmentSlots.js` | آداپتور سوم `adaptResourceSlots` با همان خروجیِ `{ start_time, end_time, label, slots }` |
|
||||||
|
| تماسها | `services/response.js` | چهار متد تازه |
|
||||||
|
| نگهداشتن | جدید | شمارش معکوس تا `expires_at`؛ معادلِ `HoldCountdown` پنل |
|
||||||
|
|
||||||
|
قواعدی که همین حالا در `nobat724_front/CLAUDE.md` هست و اینجا هم برقرارند: مدت **دادهٔ
|
||||||
|
سرور** است و در فرانت جمع زده نمیشود؛ مرزهای شیفت در فرانت استنتاج نمیشوند.
|
||||||
|
|
||||||
|
رفتارهای لبهای که باید در UI دیده شوند:
|
||||||
|
|
||||||
|
- `409` روی hold ⇒ «این زمان همین لحظه گرفته شد» + رفرش خودکار زمانها.
|
||||||
|
- انقضای hold پیش از پرداخت ⇒ برگشت به گام ۳ با پیام روشن، نه خطای خام.
|
||||||
|
- ترک صفحه ⇒ آزادسازی hold (`beforeunload` + TTL سمت سرور بهعنوان تور ایمنی).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۶. داده و مهاجرت
|
||||||
|
|
||||||
|
فاز ۱ **هیچ مهاجرتی ندارد**: `resource_occupancy`، `appointment_holds` و
|
||||||
|
`price_snapshots` از قبل هستند. تنها مهاجرتِ احتمالی، فیلد `online_bookable` روی
|
||||||
|
`clinic_resources` در فاز ۲ است (پیشفرض `true`، عقبرو-سازگار).
|
||||||
|
|
||||||
|
یک بدهیِ شناختهشده که این کار آن را برجسته میکند: نوبتهای مسیر پنلیِ منبع
|
||||||
|
(`appointments.resource_id`) ردیف `resource_occupancy` نمیسازند، پس موتور آنها را
|
||||||
|
نمیبیند ([appointment.md](../api/appointment.md)). تا وقتی رزرو آنلاین منبعمحور روشن
|
||||||
|
نشده این فقط یک ناهماهنگی است؛ **بعد از آن، منبعِ دابلبوکینگ میشود.** بستنش
|
||||||
|
پیشنیازِ فاز ۱ است، نه کارِ بعدی.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۷. فازبندی
|
||||||
|
|
||||||
|
| فاز | کار | خروجی |
|
||||||
|
|---|---|---|
|
||||||
|
| ۰ | نوشتن occupancy برای نوبتهای منبعِ پنلی + آزادسازی روی لغو | یک منبعِ حقیقت برای اشغال |
|
||||||
|
| ۱ | چهار اندپوینت عمومی + گاردها + تست (موفق/خطا/مرزی) + `docs/api/*` | API آمادهٔ مصرف |
|
||||||
|
| ۲ | سایت: آداپتور، مراحل، شمارش معکوس، حالتهای لبه | رزرو آنلاین قابل استفاده |
|
||||||
|
| ۳ | `ClinicResource.online_bookable` + نمایش «اتاق/پزشک» در تأیید نهایی | کنترل ریزتر |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۸. تصمیمهای باز
|
||||||
|
|
||||||
|
1. **بیعانه/پرداخت آنلاین:** آیا رزرو منبعمحور مثل بقیه بیعانه میگیرد؟ اگر بله، مهلت
|
||||||
|
hold باید از مهلت درگاه بیشتر باشد وگرنه بیمار پول میدهد و وقت را از دست میدهد.
|
||||||
|
2. **مهلت hold:** مقدار فعلی پنلی برای بیمارِ در حال تایپ کوتاه است. عدد جدا برای مسیر
|
||||||
|
عمومی؟
|
||||||
|
3. **بیمه:** مسیر عمومی امروز بیمه نمیگیرد؛ نوبت منبعمحور فاکتور لحظهای دارد
|
||||||
|
(`PriceSnapshot`). تکلیف سهم بیمار در سایت باید روشن شود.
|
||||||
|
4. **چند سرویس در یک نوبت آنلاین:** پنل اجازه میدهد؛ سایت هم؟ اگر بله، سقفِ مدت لازم است.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۹. چکلیست پذیرش
|
||||||
|
|
||||||
|
- [ ] کلینیک با `booking_mode = resource` و `online_booking_enabled = true` در سایت قابل رزرو است.
|
||||||
|
- [ ] خاموشکردن سوییچ، بلافاصله رزرو آنلاین را میبندد و **پنل دستنخورده** کار میکند.
|
||||||
|
- [ ] پاسخ عمومی هیچ نام منبعی افشا نمیکند.
|
||||||
|
- [ ] دو رزرو همزمان روی آخرین ظرفیت ⇒ یکی `409` میگیرد، نه هر دو موفق.
|
||||||
|
- [ ] نوبتِ ثبتشده از سایت، در تایملاین منبعِ پنل دیده میشود و بالعکس.
|
||||||
|
- [ ] تاریخ خارج از پنجرهٔ رزرو، در تقویم غیرفعال است.
|
||||||
@@ -226,8 +226,20 @@ class AdminApiController extends BaseController
|
|||||||
->from(User::class, 'u');
|
->from(User::class, 'u');
|
||||||
|
|
||||||
if ($search !== '') {
|
if ($search !== '') {
|
||||||
$qb->andWhere('u.mobileNumber LIKE :s OR u.realName LIKE :s OR u.email LIKE :s')
|
// Persian/Arabic digits are what an admin actually types into the box; normalise
|
||||||
->setParameter('s', '%' . $search . '%');
|
// them so both the mobile LIKE and the numeric user-id match behave.
|
||||||
|
$search = InputValidator::toEnglishDigits($search);
|
||||||
|
|
||||||
|
// A digits-only term is ambiguous: it can be a user id (#123) or part of a
|
||||||
|
// mobile number, so match either.
|
||||||
|
if (ctype_digit($search)) {
|
||||||
|
$qb->andWhere('u.id = :sid OR u.mobileNumber LIKE :s OR u.realName LIKE :s OR u.email LIKE :s')
|
||||||
|
->setParameter('sid', (int) $search);
|
||||||
|
} else {
|
||||||
|
$qb->andWhere('u.mobileNumber LIKE :s OR u.realName LIKE :s OR u.email LIKE :s');
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb->setParameter('s', '%' . $search . '%');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($role !== '') {
|
if ($role !== '') {
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Admin;
|
||||||
|
|
||||||
|
use App\Tests\ApiTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/admin/users?search=… — the search box must accept a numeric user id
|
||||||
|
* on top of name / mobile / email.
|
||||||
|
*/
|
||||||
|
class AdminUsersSearchTest extends ApiTestCase
|
||||||
|
{
|
||||||
|
private function idsOf(array $body): array
|
||||||
|
{
|
||||||
|
return array_map(static fn(array $u) => $u['id'], $body['data']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSearchByUserIdReturnsThatUserOnly(): void
|
||||||
|
{
|
||||||
|
$target = $this->createUser(['ROLE_USER']);
|
||||||
|
$other = $this->createUser(['ROLE_USER']);
|
||||||
|
$admin = $this->createUser(['ROLE_ADMIN']);
|
||||||
|
|
||||||
|
$body = $this->authJson('GET', '/api/v1/admin/users?search=' . $target->getId(), $admin);
|
||||||
|
|
||||||
|
self::assertSame(200, $this->responseCode());
|
||||||
|
self::assertTrue($body['success']);
|
||||||
|
self::assertContains($target->getId(), $this->idsOf($body));
|
||||||
|
self::assertNotContains($other->getId(), $this->idsOf($body));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSearchByPersianDigitsMatchesUserId(): void
|
||||||
|
{
|
||||||
|
$target = $this->createUser(['ROLE_USER']);
|
||||||
|
$admin = $this->createUser(['ROLE_ADMIN']);
|
||||||
|
|
||||||
|
$persianId = strtr((string) $target->getId(), [
|
||||||
|
'0' => '۰', '1' => '۱', '2' => '۲', '3' => '۳', '4' => '۴',
|
||||||
|
'5' => '۵', '6' => '۶', '7' => '۷', '8' => '۸', '9' => '۹',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = $this->authJson('GET', '/api/v1/admin/users?search=' . urlencode($persianId), $admin);
|
||||||
|
|
||||||
|
self::assertSame(200, $this->responseCode());
|
||||||
|
self::assertContains($target->getId(), $this->idsOf($body));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSearchByMobileStillWorks(): void
|
||||||
|
{
|
||||||
|
$target = $this->createUser(['ROLE_USER']);
|
||||||
|
$admin = $this->createUser(['ROLE_ADMIN']);
|
||||||
|
|
||||||
|
$body = $this->authJson('GET', '/api/v1/admin/users?search=' . $target->getMobileNumber(), $admin);
|
||||||
|
|
||||||
|
self::assertSame(200, $this->responseCode());
|
||||||
|
self::assertContains($target->getId(), $this->idsOf($body));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSearchByUnknownIdReturnsEmptyList(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser(['ROLE_ADMIN']);
|
||||||
|
|
||||||
|
$body = $this->authJson('GET', '/api/v1/admin/users?search=999999999', $admin);
|
||||||
|
|
||||||
|
self::assertSame(200, $this->responseCode());
|
||||||
|
self::assertSame([], $body['data']);
|
||||||
|
self::assertSame(0, $body['meta']['totalRecords']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNonAdminForbidden(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser(['ROLE_USER']);
|
||||||
|
$this->authJson('GET', '/api/v1/admin/users?search=1', $user);
|
||||||
|
self::assertSame(403, $this->responseCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user