feat(patient): implement record number pattern management
- Add RecordNumberSettingsController for managing patient record number patterns. - Create RecordNumberPattern entity to represent the pattern configuration. - Implement RecordNumberPatternRepository for database interactions. - Develop RecordNumberGenerator service for generating and validating record numbers. - Add tests for record number generation, backfilling, and API interactions. - Ensure proper access control for viewing and updating patterns based on user roles.
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
# الگوی شماره پرونده — تنظیمات per-tenant + تولید خودکار شماره پرونده
|
||||
|
||||
## پروژه
|
||||
|
||||
`clinicpro` (Backend Symfony + پنل ادمین React). cross-repo نیست: `record_number` در `nobat724_front` مصرف نمیشود (بررسی شد).
|
||||
|
||||
## زمینه
|
||||
|
||||
`PatientRecord` از قبل ستون `record_number` دارد، ولی امروز:
|
||||
|
||||
- **متن آزاد و دستی** است: کاربر در فرم ساخت پرونده تایپش میکند و zod فقط `min(1)` را چک میکند.
|
||||
- **هیچ قید یکتایی ندارد**: تنها unique روی `patient_records` جفت `(entity_type, entity_id, user_id)` است، نه شماره پرونده. دو پرونده میتوانند شمارهٔ یکسان بگیرند.
|
||||
- **پروندههای خودکار اصلاً شماره نمیگیرند**: وقتی نوبت قطعی میشود، `PatientService` پرونده را `new PatientRecord(...)` میسازد و `record_number` را `null` میگذارد. یعنی بخش بزرگی از پروندهها بیشمارهاند.
|
||||
|
||||
صاحب کلینیک/پزشک میخواهد یک **روند** داشته باشد: الگوی شماره را یکبار تعریف کند و از آن به بعد هر پروندهای که ثبت میشود خودش شماره بگیرد.
|
||||
|
||||
## مشکل / هدف
|
||||
|
||||
۱. یک تنظیمِ per-tenant «الگوی شماره پرونده» با توکنهای مشخص و شمارندهٔ خودکار.
|
||||
۲. تولید شمارهٔ پرونده **سمت سرور** در **هر دو** مسیر ساخت پرونده (دستی از پنل، خودکار از نوبت).
|
||||
۳. یکتایی شماره در هر محیط، حتی زیر درخواستهای همزمان.
|
||||
|
||||
### تصمیمهای گرفتهشده (توسط کاربر — تغییرشان ندهید)
|
||||
|
||||
| تصمیم | مقدار |
|
||||
|---|---|
|
||||
| ریست شمارنده | قابل انتخاب: `none` / `yearly` / `monthly` (بر مبنای تقویم **شمسی**) |
|
||||
| ورود دستی شماره | فقط **صاحب محیط** (پزشکِ مالک مطب / مالک کلینیک) و `ROLE_ADMIN`. منشی و پرسنل و پزشکِ مهمان → فقط شمارهٔ تولیدشده |
|
||||
| پروندههای قدیمیِ بیشماره | **lazy backfill**: اولین بار که پرونده باز میشود (`GET /api/v1/patient/{uuid}`) شماره میگیرد |
|
||||
|
||||
## معیار پذیرش
|
||||
|
||||
- ✅ **موفق (تنظیمات):** `PUT /api/v1/patient-record-number-settings` با بدنهٔ `{enabled: true, pattern: "MD-{YY}-{SEQ:4}", reset_policy: "yearly"}` توسط مالک کلینیک → `200` و `data.preview === "MD-05-0001"` (برای سال ۱۴۰۵ و شمارندهٔ صفر). `GET` همان مسیر مقدار ذخیرهشده + `next_preview` را برمیگرداند.
|
||||
- ✅ **موفق (ساخت دستی):** با الگوی فعال، `POST /api/v1/patient` بدون فیلد `record_number` → `201` و `data.record_number === "MD-05-0001"`؛ پروندهٔ بعدی `MD-05-0002`.
|
||||
- ✅ **موفق (ساخت خودکار):** قطعیکردن یک نوبت که پرونده ندارد → پروندهٔ ساختهشده توسط `PatientService` هم `record_number` غیر `null` دارد و در همان دنبالهٔ شمارهها است.
|
||||
- ✅ **موفق (backfill تنبل):** پروندهای با `record_number = null` که پیش از فعالشدن الگو ساخته شده، بعد از یک `GET /api/v1/patient/{uuid}` شماره میگیرد و در `GET` دوم **همان** شماره برمیگردد (نه شمارهٔ تازه).
|
||||
- ❌ **خطا (الگوی نامعتبر):** `PUT` با `pattern: "MD-{FOO}"` → `422` با `field: pattern` و پیام فارسی؛ `pattern` بدون هیچ `{SEQ}` → `422` (وگرنه همهٔ پروندهها یک شماره میگرفتند).
|
||||
- ❌ **خطا (دسترسی):** منشیِ همان کلینیک روی `PUT` تنظیمات → `403`. منشیای که در `POST /api/v1/patient` فیلد `record_number` میفرستد → `403` (یا فیلد بیصدا نادیده گرفته نشود؛ خطا صریح باشد).
|
||||
- ⚠️ **مرزی (همزمانی):** دو `POST /api/v1/patient` همزمان در یک محیط → دو شمارهٔ **متفاوت**؛ هیچکدام ۵۰۰ نمیدهد.
|
||||
- ⚠️ **مرزی (ریست سالانه):** با `reset_policy: yearly`، اولین پروندهٔ سال شمسی بعدی دوباره از `{SEQ}=1` شروع میشود و با پروندهٔ سال قبل تداخل ندارد (چون `{YY}` در الگوست).
|
||||
- ⚠️ **مرزی (الگوی خاموش):** با `enabled: false` رفتار امروز حفظ میشود — شماره تولید نمیشود و ورود دستی برای همه باز است.
|
||||
- ⚠️ **مرزی (سرریز شمارنده):** `{SEQ:3}` وقتی شمارنده به ۱۰۰۰ میرسد → شماره بدون پدینگ اضافه ادامه پیدا کند (`1000`)، نه اینکه بریده شود یا خطا بدهد.
|
||||
|
||||
## فایلهای مرتبط
|
||||
|
||||
| فایل | نقش |
|
||||
|------|-----|
|
||||
| `clinicpro/src/Patient/Entity/PatientRecord.php` | ستون `record_number` (خط ۴۸) — هدفِ مقداردهی |
|
||||
| `clinicpro/src/Patient/Controller/PatientController.php` | `POST /api/v1/patient` (خط ~۷۸۴)، `GET /api/v1/patient/{uuid}` (خط ۷۹۹)، `PATCH` (خط ~۹۳۵) |
|
||||
| `clinicpro/src/Patient/Service/PatientService.php` | ساخت خودکار پرونده از نوبت (خط ~۲۱۱) |
|
||||
| `clinicpro/src/Insurance/Entity/TenantServiceCategorySetting.php` | **الگوی مرجع** برای یک تنظیم per-tenant |
|
||||
| `clinicpro/src/Representation/Service/JalaliDateService.php` | تبدیل شمسی (`gregorianToJalali`) برای توکنهای `{YY}`/`{MM}` |
|
||||
| `clinicpro/src/Shared/Context/EntityContextResolver.php` | `ownedEntity($user)` — تشخیص «صاحب محیط» برای گیتِ ورود دستی |
|
||||
| `clinicpro/assets/admin/pages/PatientRecordFormPage.tsx` | فیلد و اعتبارسنجی `record_number` (خطوط ۲۲، ۱۱۹) |
|
||||
| `clinicpro/assets/admin/components/layout/settingsMenu.ts` | افزودن آیتم تنظیمات |
|
||||
| `clinicpro/docs/api/patient.md` | مستند endpointها |
|
||||
|
||||
## وضعیت فعلی
|
||||
|
||||
**`src/Patient/Entity/PatientRecord.php:44-49`** — ستون بدون قید یکتایی:
|
||||
|
||||
```php
|
||||
// Clinic-scoped case-file number. Patient identity/demographics (gender,
|
||||
// date_of_birth, referral_source, description, insurance, …) live on the
|
||||
// patient's UserProfile and are set via PATCH /patient/{uuid}.
|
||||
#[ORM\Column(name: 'record_number', type: 'string', length: 40, nullable: true)]
|
||||
private ?string $recordNumber = null;
|
||||
```
|
||||
|
||||
**`src/Patient/Controller/PatientController.php:784-795`** — ساخت دستی، شماره فقط اگر کاربر فرستاده باشد:
|
||||
|
||||
```php
|
||||
$record = new PatientRecord($entityType, $entityId, $patient, $user->hasRole('ROLE_DOCTOR') ? 'doctor' : 'clinic', $entityId);
|
||||
|
||||
if (($rn = trim((string) ($data['record_number'] ?? ''))) !== '') {
|
||||
$record->setRecordNumber($rn);
|
||||
}
|
||||
$tagError = $this->applyRecordTags($record, $data, $entityType, $entityId);
|
||||
if ($tagError !== null) {
|
||||
return $tagError;
|
||||
}
|
||||
|
||||
$this->recordRepo->save($record);
|
||||
|
||||
return $this->success($record->toArray(), 201);
|
||||
```
|
||||
|
||||
**`src/Patient/Service/PatientService.php:209-213`** — ساخت خودکار، بدون هیچ شمارهای:
|
||||
|
||||
```php
|
||||
$record = $this->recordRepo->findByEntityAndUser($entityType, $entityId, $patient);
|
||||
if ($record === null) {
|
||||
$record = new PatientRecord($entityType, $entityId, $patient, 'system', $createdById);
|
||||
$this->recordRepo->save($record);
|
||||
}
|
||||
```
|
||||
|
||||
**`assets/admin/pages/PatientRecordFormPage.tsx:22,119-120`** — فیلد دستیِ الزامی:
|
||||
|
||||
```tsx
|
||||
record_number: z.string().min(1, 'شماره پرونده الزامی است'),
|
||||
...
|
||||
<Field label="شماره پرونده" required error={form.formState.errors.record_number?.message}>
|
||||
<div className="field"><input {...form.register('record_number')} placeholder="شماره پرونده" /></div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. Entity + migration تنظیمات الگو
|
||||
|
||||
`src/Patient/Entity/RecordNumberPattern.php` — یک ردیف به ازای هر محیط، دقیقاً با سبک `TenantServiceCategorySetting` (همان `entity_type/entity_id` + unique constraint، timestamp عدد صحیح):
|
||||
|
||||
```php
|
||||
#[ORM\Entity(repositoryClass: RecordNumberPatternRepository::class)]
|
||||
#[ORM\Table(name: 'record_number_patterns')]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_record_number_pattern', columns: ['entity_type', 'entity_id'])]
|
||||
class RecordNumberPattern
|
||||
{
|
||||
public const RESET_NONE = 'none';
|
||||
public const RESET_YEARLY = 'yearly';
|
||||
public const RESET_MONTHLY = 'monthly';
|
||||
|
||||
// entity_type, entity_id, enabled(bool), pattern(string 60),
|
||||
// reset_policy(string 10), counter(int), counter_period(string 7, nullable),
|
||||
// updated_at(int)
|
||||
}
|
||||
```
|
||||
|
||||
`counter_period` مهر دورهٔ فعلیِ شمارنده است (`'1405'` برای yearly، `'1405-05'` برای monthly، `null` برای none). با ورود دورهٔ جدید، `counter` صفر و `counter_period` بهروز میشود — بدون این ستون، «آیا سال عوض شده؟» فقط با حدس از `updated_at` قابل جواب بود.
|
||||
|
||||
سپس:
|
||||
|
||||
```bash
|
||||
ddev exec php bin/console doctrine:migrations:diff --no-interaction
|
||||
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
|
||||
```
|
||||
|
||||
**نحوه تست:** `ddev exec php bin/console doctrine:schema:validate` سبز باشد؛ جدول با `ddev exec mysql -uroot -proot db -e "DESCRIBE record_number_patterns;"` وجود داشته باشد.
|
||||
|
||||
---
|
||||
|
||||
### ۲. یکتایی شمارهٔ پرونده در سطح دیتابیس
|
||||
|
||||
قبل از افزودن ایندکس، تداخلهای موجود را ببین:
|
||||
|
||||
```sql
|
||||
SELECT entity_type, entity_id, record_number, COUNT(*) c
|
||||
FROM patient_records WHERE record_number IS NOT NULL
|
||||
GROUP BY 1,2,3 HAVING c > 1;
|
||||
```
|
||||
|
||||
اگر ردیفی برگشت، **به کاربر گزارش بده و متوقف شو** — پاککردن خودسرانهٔ شمارهٔ پروندهٔ واقعی مجاز نیست. اگر خالی بود، unique index روی `(entity_type, entity_id, record_number)` اضافه کن (MariaDB چند `NULL` را در unique میپذیرد، پس پروندههای بیشماره مانع نمیشوند).
|
||||
|
||||
این ایندکس **تور ایمنی** است، نه مکانیزم اصلی؛ مکانیزم اصلی قفلِ وظیفهٔ ۳ است.
|
||||
|
||||
**نحوه تست:** درج دستی دو ردیف با شمارهٔ یکسان در یک محیط → خطای دیتابیس.
|
||||
|
||||
---
|
||||
|
||||
### ۳. سرویس تولید شماره — `RecordNumberGenerator`
|
||||
|
||||
`src/Patient/Service/RecordNumberGenerator.php`، تنها جایی که شماره ساخته میشود. سه مسئولیت جدا:
|
||||
|
||||
**الف) رندر الگو (خالص، بدون I/O):**
|
||||
|
||||
توکنهای مجاز — هر چیز دیگری `422`:
|
||||
|
||||
| توکن | معنی |
|
||||
|---|---|
|
||||
| `{YY}` | دو رقم آخر سال شمسی (`05`) |
|
||||
| `{YYYY}` | سال شمسی کامل (`1405`) |
|
||||
| `{MM}` | ماه شمسی دو رقمی (`05`) |
|
||||
| `{SEQ}` / `{SEQ:n}` | شمارنده، با `n` رقم پدینگ صفر (پیشفرض ۱) |
|
||||
|
||||
سال/ماه شمسی از `JalaliDateService::gregorianToJalali()` میآید — **پیادهسازی دستی ننویس**؛ همان فایل توضیح میدهد نسخهٔ دستساز قبلی غلط بود.
|
||||
|
||||
**ب) گرفتن شمارهٔ بعدی (تراکنشی):**
|
||||
|
||||
```php
|
||||
public function next(string $entityType, int $entityId): ?string
|
||||
{
|
||||
// بدون الگو یا با enabled=false → null (رفتار امروز)
|
||||
return $this->em->wrapInTransaction(function () use ($entityType, $entityId): ?string {
|
||||
$pattern = $this->repo->lockForUpdate($entityType, $entityId); // SELECT ... FOR UPDATE
|
||||
if ($pattern === null || !$pattern->isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
$period = $this->periodKey($pattern->getResetPolicy());
|
||||
if ($pattern->getCounterPeriod() !== $period) {
|
||||
$pattern->resetCounter($period);
|
||||
}
|
||||
$pattern->incrementCounter();
|
||||
|
||||
return $this->render($pattern->getPattern(), $pattern->getCounter());
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
قفلِ ردیف (`LockMode::PESSIMISTIC_WRITE`) شرطِ معیار پذیرشِ «دو درخواست همزمان» است؛ خواندن-افزایش-نوشتن بدون قفل، دو شمارهٔ یکسان میدهد.
|
||||
|
||||
**ج) پیشنمایش (بدون افزایش شمارنده):** `preview(string $pattern, string $resetPolicy): string` برای صفحهٔ تنظیمات.
|
||||
|
||||
**نکتهٔ SOLID:** این سرویس با constructor injection به `RecordNumberPatternRepository` و `JalaliDateService` وابسته است؛ `new` ممنوع. Controller و `PatientService` هر دو فقط `next()` را صدا میزنند — منطق دو جا کپی نشود.
|
||||
|
||||
**نحوه تست:** unit test روی `render()` برای هر توکن + سرریز `{SEQ:3}` روی ۱۰۰۰؛ و یک functional test که دو بار `next()` را صدا بزند و دو شمارهٔ متوالی بگیرد.
|
||||
|
||||
---
|
||||
|
||||
### ۴. Endpointهای تنظیمات
|
||||
|
||||
در `src/Patient/Controller/` (کنترلر جدید `RecordNumberSettingsController extends BaseController`):
|
||||
|
||||
| Method | Route | دسترسی |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/v1/patient-record-number-settings` | هر کاربرِ محیط با `patients.view` |
|
||||
| `PUT` | `/api/v1/patient-record-number-settings` | فقط صاحب محیط یا `ROLE_ADMIN` |
|
||||
|
||||
پاسخ `GET`:
|
||||
|
||||
```json
|
||||
{ "success": true, "data": {
|
||||
"enabled": true, "pattern": "MD-{YY}-{SEQ:4}", "reset_policy": "yearly",
|
||||
"counter": 12, "next_preview": "MD-05-0013", "can_edit": true
|
||||
} }
|
||||
```
|
||||
|
||||
`can_edit` را از `EntityContextResolver::ownedEntity($user)` بساز (بررسی کن که این متد واقعاً همان چیزی را میدهد که لازم است؛ اگر نه، از `ClinicDoctorAccessChecker`/مالکِ `Clinic::getUser()` استفاده کن). پنل با همین فلگ فرم را read-only میکند — ولی **گیت اصلی سمت سرور است**، نه UI.
|
||||
|
||||
اعتبارسنجی `PUT`: `pattern` غیرخالی، حداکثر ۶۰ نویسه، فقط توکنهای مجاز، حتماً شامل `{SEQ...}`، و `reset_policy` یکی از سه مقدار. هر خطا `422` با `field` درست از `$this->validationError()`/`$this->error()`.
|
||||
|
||||
**نحوه تست:**
|
||||
|
||||
```bash
|
||||
T=$(ddev exec php bin/console lexik:jwt:generate-token -c 'App\\Auth\\Entity\\User' -- 09390039833)
|
||||
curl -sk -X PUT https://clinic-pro.ddev.site/api/v1/patient-record-number-settings \
|
||||
-H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
|
||||
-d '{"enabled":true,"pattern":"MD-{YY}-{SEQ:4}","reset_policy":"yearly"}'
|
||||
# سپس همان با توکن منشی 09390039875 → باید 403 بدهد
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۵. اتصال به هر دو مسیر ساخت پرونده
|
||||
|
||||
**`PatientController::create`** — منطق فعلیِ خط ۷۸۶ عوض میشود:
|
||||
|
||||
```php
|
||||
$manual = trim((string) ($data['record_number'] ?? ''));
|
||||
if ($manual !== '' && !$this->canSetRecordNumberManually($user, $entityType, $entityId)) {
|
||||
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'ثبت دستی شماره پرونده مجاز نیست', 403, 'record_number');
|
||||
}
|
||||
$record->setRecordNumber($manual !== '' ? $manual : $this->recordNumbers->next($entityType, $entityId));
|
||||
```
|
||||
|
||||
**`PatientService`** (خط ~۲۱۱) — همان یک خط:
|
||||
|
||||
```php
|
||||
$record = new PatientRecord($entityType, $entityId, $patient, 'system', $createdById);
|
||||
$record->setRecordNumber($this->recordNumbers->next($entityType, $entityId));
|
||||
$this->recordRepo->save($record);
|
||||
```
|
||||
|
||||
بدون این دومی، خواستهٔ «هر پروندهای که ثبت میشود» برآورده نمیشود — بیشترِ پروندهها از همین مسیرِ نوبت ساخته میشوند.
|
||||
|
||||
**نحوه تست:** با الگوی فعال یک `POST /api/v1/patient` بدون `record_number` بزن و شماره را ببین؛ سپس یک نوبت را قطعی کن (`POST /api/v1/appointment/{uuid}/confirm`) و شمارهٔ پروندهٔ ساختهشده را از `GET /api/v1/patients?search=...` بخوان.
|
||||
|
||||
---
|
||||
|
||||
### ۶. Backfill تنبل هنگام باز شدن پرونده
|
||||
|
||||
در `PatientController::show` (خط ۷۹۹)، **قبل از** ساخت پاسخ:
|
||||
|
||||
```php
|
||||
if ($record->getRecordNumber() === null) {
|
||||
$number = $this->recordNumbers->next($entityType, $entityId);
|
||||
if ($number !== null) {
|
||||
$record->setRecordNumber($number);
|
||||
$this->recordRepo->save($record);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
سه نکته که باید رعایت شود:
|
||||
|
||||
- فقط وقتی الگو فعال است (`next()` خودش `null` برمیگرداند) — وگرنه یک `GET` ساده تبدیل به نوشتن بیدلیل میشود.
|
||||
- idempotent: بار دوم چون شماره پر است، هیچ نوشتنی رخ نمیدهد. (معیار پذیرشِ «GET دوم همان شماره»)
|
||||
- دو `GET` همزمان روی یک پرونده: قفلِ وظیفهٔ ۳ دو شمارهٔ متفاوت میدهد ولی آخرین نوشتن برنده است و یک شماره هدر میرود. قابل قبول است؛ **در کد کامنت شود** که چرا (هدررفتِ یک شماره در برابر پیچیدگی قفل روی خودِ رکورد).
|
||||
|
||||
**نحوه تست:** یک `patient_records` با `record_number = NULL` در DB پیدا/بساز، دو بار `GET /api/v1/patient/{uuid}` بزن، هر دو بار شمارهٔ یکسان برگردد.
|
||||
|
||||
---
|
||||
|
||||
### ۷. پنل ادمین — صفحهٔ تنظیمات + فرم پرونده
|
||||
|
||||
**الف) صفحهٔ تنظیمات** `assets/admin/pages/RecordNumberSettingsPage.tsx`:
|
||||
|
||||
- داخل `SettingsLayout` مثل بقیهٔ صفحات تنظیمات.
|
||||
- سوییچ «شمارهگذاری خودکار پرونده»، ورودی الگو، `SearchableSelect` برای سیاست ریست (**نه** `<select>` بومی — قاعدهٔ پروژه)، و یک خط پیشنمایشِ زنده: «شمارهٔ بعدی: MD-05-0013».
|
||||
- راهنمای کوتاه توکنها زیر فیلد.
|
||||
- وقتی `can_edit === false`: فرم read-only + یک خط توضیح که فقط صاحب حساب میتواند تغییر دهد.
|
||||
- route در `App.tsx` با `RoleRoute roles={['doctor','clinic','secretary']} blockClinicScope permission={['patients','view']}` و آیتم در `settingsMenu.ts` (کلید `record-number`، آیکون `HashtagIcon`، همان `perm`).
|
||||
|
||||
**ب) فرم پرونده** `PatientRecordFormPage.tsx`:
|
||||
|
||||
- وقتی الگو فعال است و کاربر اجازهٔ دستی ندارد: فیلد `record_number` **در حالت ساخت** پنهان/read-only شود و قاعدهٔ zod از `min(1)` به اختیاری تغییر کند — وگرنه فرم با فیلدی که کاربر نمیتواند پر کند قفل میشود. در حالت ویرایش، رفتار فعلی برای صاحب حساب میماند.
|
||||
- مقدار الگو/دسترسی از همان `GET /api/v1/patient-record-number-settings` (یک هوک `useRecordNumberSettings` در `assets/admin/hooks/`).
|
||||
|
||||
**نحوه تست:** `npx tsc --noEmit` + `npx vitest run assets/admin/pages/RecordNumberSettingsPage.test.tsx` با سه سناریو: ذخیرهٔ الگو، پیشنمایش درست، و read-only بودن فرم وقتی `can_edit === false`. برای فرم پرونده یک تست که با الگوی فعال، ارسال بدون `record_number` را مجاز بداند.
|
||||
|
||||
---
|
||||
|
||||
### ۸. مستندات
|
||||
|
||||
- `docs/api/patient.md`: بخش تازه برای دو endpoint تنظیمات (method/path/permission، بدنهٔ کامل request، JSON واقعیِ response از اجرای واقعی، همهٔ کدهای خطا) + بهروزرسانی توضیح `record_number` در `POST /api/v1/patient` و `PATCH /patient/{uuid}` (چه کسی میتواند دستی بفرستد، چه زمانی سرور تولید میکند).
|
||||
- در همان فایل بنویس که `GET /api/v1/patient/{uuid}` ممکن است شمارهٔ پرونده را تخصیص دهد (اثر جانبیِ عمدی روی یک `GET` — سند بدون این، رفتار را غافلگیرکننده میکند).
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- **الگوی طراحی:** یک سرویسِ تکی (`RecordNumberGenerator`) کافی است؛ Strategy برای «انواع الگو» نساز — الان فقط یک زبانِ توکن وجود دارد و abstraction دوم مصرف ندارد (guidelines §۵). ریستها فقط سه مقدار ثابتاند و با یک `match` روی `periodKey()` حل میشوند، نه سه کلاس.
|
||||
- **جداسازی محیط:** جدول جدید `entity_type/entity_id` دارد، پس یا `TenantOwnedTrait` بگیرد یا با دلیل در `GlobalTables` ثبت شود — `TenantSchemaCoverageTest` در غیر این صورت قرمز میشود. (مسیر درست: `TenantOwnedTrait`.)
|
||||
- **شمارنده روی همان ردیفِ تنظیمات است**، نه `MAX(record_number)+1`. شمارش از روی مقادیر موجود، با شمارهٔ دستیِ صاحب حساب یا با تغییر الگو میشکند.
|
||||
- **گیتِ ورود دستی سمت سرور اجباری است.** پنهانکردن فیلد در UI کافی نیست؛ منشی میتواند مستقیم به API بزند.
|
||||
- **رفتار قبلی نباید بشکند:** محیطی که الگو ندارد یا `enabled=false` است باید دقیقاً مثل امروز کار کند (ورود دستی آزاد، شماره تولید نشود). این را تست کن.
|
||||
- **`record_number` در جستجوی بیماران استفاده میشود** (`/api/v1/patients?search=`, placeholder «جستجوی نام، شماره تماس، شماره پرونده...»). بعد از این تغییر، جستجو با شمارهٔ تولیدشده را دستی امتحان کن.
|
||||
- **مصرفکنندهٔ دیگر ندارد:** `nobat724_front` و `clinic-pro-tauri` این فیلد را نمیخوانند (grep شد)، پس تغییر cross-repo لازم نیست — ولی اگر در حین کار خلافش دیده شد، گزارش بده.
|
||||
Reference in New Issue
Block a user