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:
hamed
2026-08-04 12:30:17 +03:30
parent 9b8eef9598
commit 85a27812c7
24 changed files with 2009 additions and 19 deletions
+80 -1
View File
@@ -117,6 +117,69 @@ restriction.
---
### الگوی شمارهٔ پرونده (2026-08)
شمارهٔ پرونده می‌تواند به‌جای ورود دستی، از یک الگوی per-tenant تولید شود. الگو و
شمارنده‌اش روی `record_number_patterns` می‌نشینند — یک ردیف به ازای هر محیط.
#### `GET /api/v1/patient-record-number-settings`
**دسترسی:** `patients.view` (منشی/پزشکِ عضو با همان مجوز). محیط از `UserActiveContext`
حل می‌شود؛ محیطِ حل‌نشده → `403 ERR_FORBIDDEN_001`.
خروجی واقعی (محیطی که هنوز چیزی ذخیره نکرده — همه‌چیز پیش‌فرض):
```json
{"success":true,"data":{"enabled":false,"pattern":"{YY}-{SEQ:4}","reset_policy":"none","counter":0,"next_preview":"05-0001","can_edit":true}}
```
| فیلد | معنی |
|---|---|
| `enabled` | تولید خودکار روشن است یا نه. خاموش = رفتار قدیمی (ورود دستی برای همه) |
| `pattern` | الگو با توکن‌های `{YYYY}` `{YY}` `{MM}` `{SEQ}` `{SEQ:n}` |
| `reset_policy` | `none` \| `yearly` \| `monthly` — بر مبنای تقویم **شمسی** |
| `counter` | شمارندهٔ فعلیِ همین دوره |
| `next_preview` | شمارهٔ بعدی، بدون مصرف‌کردن شمارنده |
| `can_edit` | آیا همین کاربر اجازهٔ `PUT` دارد (صاحب محیط یا ادمین) |
#### `PUT /api/v1/patient-record-number-settings`
**دسترسی:** فقط **صاحب محیط** (مالک کلینیک، یا پزشک در مطب شخصی خودش) و `ROLE_ADMIN`.
پزشکِ مهمانِ کلینیک و منشی — حتی با `patients.update` — رد می‌شوند: شمارهٔ پرونده
قرارداد ثبتِ کل مجموعه است.
| فیلد | نوع | الزامی | قاعده |
|---|---|---|---|
| `enabled` | bool | — | پیش‌فرض `false` |
| `pattern` | string | ✅ | حداکثر ۶۰ نویسه، فقط توکن‌های مجاز، حتماً شامل `{SEQ...}` |
| `reset_policy` | string | — | `none` \| `yearly` \| `monthly`، پیش‌فرض `none` |
خروجی واقعی:
```json
{"success":true,"data":{"enabled":true,"pattern":"MD-{YY}-{SEQ:4}","reset_policy":"yearly","counter":0,"next_preview":"MD-05-0001","can_edit":true}}
```
**Errors** (پیام‌ها واقعی‌اند — از اجرای همین اندپوینت):
| Code | HTTP | field | شرط |
|------|------|-------|-----|
| `ERR_FORBIDDEN_001` | 403 | — | «تغییر الگوی شماره پرونده فقط با حساب صاحب مجموعه ممکن است» |
| `ERR_VALIDATION_002` | 422 | `pattern` | «توکن ناشناخته: {FOO} — مجاز: {YYYY}، {YY}، {MM}، {SEQ} یا {SEQ:n}» |
| `ERR_VALIDATION_002` | 422 | `pattern` | «الگو باید {SEQ} یا {SEQ:n} داشته باشد، وگرنه همهٔ پرونده‌ها یک شماره می‌گیرند» |
| `ERR_VALIDATION_002` | 422 | `pattern` | «وقتی سال در الگو هست، ریست شمارنده باید سالانه یا ماهانه باشد» |
| `ERR_VALIDATION_002` | 422 | `pattern` | «وقتی {MM} در الگو هست، ریست شمارنده باید ماهانه باشد» |
| `ERR_VALIDATION_002` | 422 | `reset_policy` | «سیاست ریست شمارنده نامعتبر است» |
| `ERR_VALIDATION_001` | 422 | — | بدنهٔ غیر-JSON |
- **تغییر الگو شمارنده را صفر نمی‌کند.** شماره‌های صادرشده وجود دارند و شروع دوبارهٔ
دنباله مستقیم به `uniq_patient_record_number` می‌خورد. صفرشدن فقط با ورود به دورهٔ
تازه (سال/ماه شمسی) رخ می‌دهد.
- **الگوی خاموش هم اعتبارسنجی می‌شود** تا خطای الگو سرِ فرمِ تنظیمات دیده شود، نه
روزی که کاربر روشنش می‌کند و ساختِ پرونده می‌شکند.
---
### Create Patient Record
```
@@ -138,13 +201,24 @@ Creates a patient record for a user under the current entity. If the record alre
"mobile": "09xxxxxxxxx (اختیاری — برای جستجو یا ساخت بیمار جدید)",
"name": "string (الزامی فقط هنگام ساخت بیمار جدید)",
"national_code": "string (اختیاری، ۱۰ رقم)",
"record_number": "string (اختیاری) — شماره پرونده، مخصوص رکورد",
"record_number": "string (اختیاری) — شماره پرونده، مخصوص رکورد. با الگوی فعال فقط از صاحب مجموعه پذیرفته می‌شود",
"tags": ["uuid برچسب‌های TenantTag (اختیاری) — باید متعلق به همین tenant باشند"]
}
```
- اگر `user_uuid` و `mobile` هر دو خالی باشند → خطا.
- `record_number` و `tags` روی خودِ رکورد ذخیره می‌شوند (نه پروفایل کاربر). سایر مشخصات دموگرافیک (`gender`, `date_of_birth`, `referral_source`, `description`, بیمه‌ها) روی `UserProfile` هستند و از طریق `PATCH /patient/{uuid}` ست می‌شوند. پاسخ همیشه `record_number` و `tags: [{uuid,name,color}]` را برمی‌گرداند.
- **شمارهٔ پرونده با الگوی فعال (2026-08):** اگر محیط الگوی فعال داشته باشد و `record_number`
فرستاده **نشود**، سرور شمارهٔ بعدیِ همان الگو را می‌سازد و در پاسخ برمی‌گرداند
(مثلاً `"record_number": "MD-0001"`). فرستادنِ `record_number` توسط کسی جز صاحب
مجموعه → `403 ERR_FORBIDDEN_001` با `field: record_number` و پیام «ثبت دستی شماره
پرونده مجاز نیست؛ شماره از الگوی مجموعه ساخته می‌شود». بدون الگوی فعال، رفتار قبلی
برقرار است: شماره دستی است و همه می‌توانند بفرستند. همین قاعده روی
`PATCH /patient/{uuid}` هم اعمال می‌شود. تنظیم الگو:
[الگوی شمارهٔ پرونده](#الگوی-شمارهٔ-پرونده-2026-08).
- **پروندهٔ خودکارِ نوبت هم شماره می‌گیرد:** پرونده‌ای که هنگام قطعی‌شدن نوبت ساخته
می‌شود (`PatientService::autoCreateOnAppointmentConfirm`) از همان دنباله شماره
می‌گیرد؛ پیش از این همیشه `null` بود.
- برچسب متعلق به tenant دیگر → `422 ERR_VALIDATION_001` (`field: tags`).
- `national_code` فقط وقتی روی کاربر ست می‌شود که کاربر کد ملی نداشته باشد.
- موبایل تکراری duplicate نمی‌سازد؛ همان کاربر استفاده می‌شود.
@@ -185,6 +259,11 @@ GET /api/v1/patient/{uuid}
Returns a single patient record, enriched with the patient's full profile (`profile`) درون‌خطی از `UserProfile`. اگر پروفایل وجود نداشت، فیلدها `null` برمی‌گردند (نه خطا). نام بیمه‌ها از روی id resolve می‌شوند.
> **اثر جانبیِ عمدی (2026-08):** اگر این پرونده `record_number = null` باشد و محیط الگوی
> فعال داشته باشد، همین `GET` شماره را تخصیص می‌دهد و ذخیره می‌کند — یعنی پرونده‌های
> ساخته‌شده پیش از الگو، اولین بار که باز می‌شوند شماره‌دار می‌شوند. فراخوانی دوم چیزی
> نمی‌نویسد و همان شماره را برمی‌گرداند. بدون الگوی فعال، این `GET` فقط می‌خواند.
**Response 200:**
```json