Files
clinicpro/.claude/prompt/session-edit-payment-crud-audit-log.md
T

108 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ویرایش سرویس‌های مراجعه + ویرایش/حذف پرداخت + Audit Log مالی جامع
## پروژه
`clinicpro` (backend Symfony + پنل ادمین React). تک-ریپو.
> تست: پنل ادمین با `09390039833 / 09390039833`. اجرا داخل ddev. قرارداد پول: ذخیره/API ریال، UI تومان (`tomanToRial`/`rialToToman`). تاریخ Unix.
## زمینه
مراجعه (`PatientSession`) فقط **ایجاد** می‌شود؛ پس از ثبت، امکان ویرایش سرویس‌ها/کالاها/قیمت ویزیت/بیمه وجود ندارد و پرداخت‌های ثبت‌شده نه ویرایش می‌شوند نه حذف. کاربر می‌خواهد بتواند همه‌ی این‌ها را ویرایش کند، **اما** هر تغییر مالی/خدماتی باید در یک **Audit Log** ثبت و قابل‌مشاهده باشد (چه کسی، چه چیزی، کِی، مقدار قبل/بعد، نوع عملیات).
## مشکل / هدف
1. ویرایش کامل یک مراجعه پس از ثبت: سرویس‌ها (افزودن/حذف/تعداد)، کالاهای مصرفی، قیمت ویزیت، بیمه/درصدها، یادداشت، تاریخ مراجعه — با محاسبه‌ی مجدد `services_total_rials`/`final_price_rials`.
2. ویرایش و حذف پرداخت‌های ثبت‌شده، با نگه‌داشتن سازگاری `paid_total`/`remaining`/`payment_method`/`paid_at`.
3. **Audit Log جامع** برای همه‌ی تغییرات مالی/خدماتی: مقدار قبل، مقدار بعد، کاربر، تاریخ/زمان، نوع عملیات (create/update/delete). نمایش تاریخچه در UI.
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `src/Patient/Service/PatientService.php` | `createSession()` (create-only، L165-265)، `addSessionPayment()` (L338-386)، `calculateFinalPrice()` (L63-93) |
| `src/Patient/Controller/PatientController.php` | `updateSession` PATCH (L1037-1092، فیلدهای محدود)، `addSessionPayment` POST (L1100-1122)؛ **بدون** endpoint ویرایش/حذف payment و ویرایش services |
| `src/Patient/Entity/SessionService.php` | خط سرویس؛ **immutable** (بدون setter)؛ constructor snapshot قیمت |
| `src/Patient/Entity/SessionConsumable.php` | خط کالا؛ immutable |
| `src/Patient/Entity/SessionPayment.php` | پرداخت؛ فقط setter برای createdBy/Name؛ `METHODS` (L18)؛ toArray (L74-84) |
| `src/Patient/Repository/SessionServiceRepository.php` / `SessionConsumableRepository.php` / `SessionPaymentRepository.php` | فقط `save()`**بدون `remove()`** |
| `src/Patient/Entity/PatientSession.php` | `getPaidTotalRials()` (L164-170)، `getRemainingRials()` (L173-176)؛ collections با `cascade:['remove']` |
| `src/Appointment/Entity/AppointmentEvent.php` + Repository + endpoint | **الگوی مرجع audit** (id, uuid, FK, type, title, actor_user_id, actor_name, reason, created_at؛ `findByAppointmentUuid`؛ `GET /appointment/{uuid}/events`) |
| `src/Settlement/Service/WalletService.php` | `resolveActorName(?User)` (L34-43) — نام نمایشی کاربر |
| `src/Shared/Constant/ErrorCodes.php` | `ERR_SESSION_NOT_FOUND`, `ERR_SESSION_PAYMENT_INVALID`, `ERR_SESSION_PAYMENT_EXCEEDS` (L64-66) |
| `assets/admin/components/SessionServiceCard.tsx` | dropdown «...» (L101-114) — محل افزودن «ویرایش» + «تاریخچه تغییرات» |
| `assets/admin/pages/PatientDetailPage.tsx` | تب services (L225-248)؛ کارت‌ها؛ `sessionsQ` |
| `assets/admin/components/session/PaymentStep.tsx` | ردیف پرداخت‌ها (L250-270) — محل ویرایش/حذف + audit |
| `assets/admin/components/session/CreateStep.tsx` | فرم ثبت (submit body L233-245) — **الگوی فرم ویرایش** |
| `assets/admin/pages/NewSessionPage.tsx` | ویزارد ثبت — قابل بازاستفاده برای ویرایش |
| `docs/api/patient.md` | مستندات |
## وضعیت فعلی
`updateSession` فیلدهای محدود می‌پذیرد (notes/archived/discount/paid_at/payment_method) — نه services/consumables/visit_price:
```php
// PatientController::updateSession (خلاصه)
if (isset($data['notes'])) { $session->setNotes($data['notes']); }
if (array_key_exists('archived', $data)) { $session->setArchived((bool)$data['archived']); }
// discount_rule_uuid / discount_type / paid_at / payment_method ...
// ← هیچ services / consumables / visit_price_rials
```
`createSession` تنها جایی است که `SessionService`/`SessionConsumable` ساخته و مجموع محاسبه می‌شود (create-only). پرداخت فقط `POST` دارد؛ `grep payments/{` صفر → نه PATCH نه DELETE.
## وظایف
### ۱. Entity + migration — `SessionAuditLog` (الگوی AppointmentEvent)
`src/Patient/Entity/SessionAuditLog.php` (جدید):
- ستون‌ها: `id`, `uuid`, `session` (ManyToOne `PatientSession`, `onDelete: CASCADE`), `field` string (مثل `visit_price_rials`, `services`, `consumables`, `payment`, `discount`), `operation` string (`create`|`update`|`delete`), `old_value` text nullable, `new_value` text nullable, `actor_user_id` int nullable, `actor_name` string nullable, `note` string nullable, `created_at` int.
- constructor `(PatientSession $session, string $field, string $operation)`؛ fluent `setActor(?int,?string)`, `setValues(?string $old, ?string $new)`, `setNote(?string)`.
- `toArray()`: `field`, `operation`, `old_value`, `new_value`, `actor_name`, `note`, `created_at`.
- Repository `SessionAuditLogRepository` با `save()` و `findBySessionUuid(string $uuid): array` (array hydration، مرتب بر `created_at DESC`).
- migration.
> مقادیر قبل/بعد را به‌صورت رشته‌ی خوانا ذخیره کن (مثلاً برای پول ریال عددی؛ برای لیست سرویس‌ها یک خلاصه مثل «تزریق ژل ×۱، لیزر ×۲» یا JSON فشرده). ثبات مهم‌تر از فرمت است.
### ۲. Service — ثبت audit + منطق ویرایش/حذف
در `PatientService`:
- helper `logSessionChange(PatientSession $s, string $field, string $op, ?string $old, ?string $new, ?User $actor, ?string $note = null)` که `SessionAuditLog` می‌سازد و ذخیره می‌کند (`actor_name` با `walletService->resolveActorName`).
- **`updateSessionServices(PatientSession $s, array $data, User $actor)`**: سرویس‌ها/کالاها/قیمت ویزیت/بیمه را جایگزین کند:
- snapshot مقادیر قبل (visit_price, services خلاصه, consumables خلاصه, services_total, final_price).
- سرویس‌های قبلی را `remove` (به `SessionServiceRepository` متد `remove()` اضافه کن)، سپس از `$data['services']` دوباره بساز (مثل createSession).
- همین برای consumables (`SessionConsumableRepository::remove()`).
- `setVisitPriceRials`, بیمه/درصدها، `session_at`, notes را ست کن.
- با `calculateFinalPrice(...)` + مجموع کالاها، `services_total_rials`/`final_price_rials` را بازمحاسبه کن (دقیقاً مثل createSession L213-240).
- برای هر فیلدِ تغییرکرده یک `logSessionChange(... 'update' ...)` با old/new بزن.
- **`updatePayment(SessionPayment $p, array $data, User $actor)`**: `method`/`amount_rials`/`paid_at` را ویرایش کند (setterها را به `SessionPayment` اضافه کن). سقف: مجموع پرداخت‌ها نباید از `final - discount` بیشتر شود. audit با field=`payment`, op=`update`, old/new = مبلغ قبل/بعد.
- **`deletePayment(SessionPayment $p, User $actor)`**: پرداخت را `remove` (به `SessionPaymentRepository::remove()`). audit op=`delete`, old=مبلغ.
- **بازمحاسبه‌ی فیلدهای کش‌شده**: بعد از ویرایش/حذف پرداخت، اگر `getRemainingRials() > 0` بود `payment_method='pending'` و `paid_at=null`؛ اگر صفر شد `payment_method`/`paid_at` را ست کن. (چون `getPaidTotalRials()` از collection زنده جمع می‌زند ولی `payment_method`/`paid_at` کش‌اند.)
- **wallet edge**: اگر پرداخت `wallet` بود، ویرایش/حذف باید تراکنش کیف پول را جبران کند (بازگشت/کسر تفاوت). اگر جبران خارج از scope است، **حذف/ویرایش پرداخت wallet را مسدود کن** (خطای ۴۲۲ با پیام فارسی) تا مغایرت مالی ایجاد نشود — این ساده‌تر و امن‌تر است؛ در پرامپت این گزینه را انتخاب کن مگر بازگشت کیف پول ساده باشد.
### ۳. Controller — endpointهای جدید
در `PatientController` (extends BaseController، owner-scope مثل `updateSession`):
- **ویرایش services**: `updateSession` را گسترش بده تا اگر `services`/`consumables`/`visit_price_rials`/insurance آمد، `patientService->updateSessionServices()` صدا زده شود؛ یا یک route جداگانه `PATCH /api/v1/session/{uuid}/services`. (گسترش `updateSession` تمیزتر است.)
- **ویرایش پرداخت**: `PATCH /api/v1/session/{uuid}/payments/{paymentUuid}``updatePayment`.
- **حذف پرداخت**: `DELETE /api/v1/session/{uuid}/payments/{paymentUuid}``deletePayment`.
- **تاریخچه**: `GET /api/v1/session/{uuid}/audit-log``$this->success($auditRepo->findBySessionUuid($uuid))`.
- گارد: پرداخت باید متعلق به همان session باشد؛ session متعلق به owner (`ownsRecord`).
### ۴. Frontend — فرم ویرایش + کنترل پرداخت + نمایش تاریخچه
1. **منوی کارت** (`SessionServiceCard.tsx` dropdown L101-114): افزودن آیتم‌های «ویرایش» و «تاریخچه تغییرات» (props جدید `onEdit(session)`, `onViewAudit(session)`).
2. **فرم ویرایش**: از `CreateStep` بازاستفاده کن (یا کامپوننت مشترک) در حالت edit؛ با داده‌ی فعلی session پر شود و به `PATCH /session/{uuid}` (با body مثل CreateStep L233-245) بفرستد. مسیر `session/{uuid}/edit` یا مودال.
3. **ردیف پرداخت** (`PaymentStep.tsx` L250-270): برای هر پرداخت آیکون ویرایش (مودال کوچک: مبلغ تومان + روش + تاریخ → `PATCH .../payments/{uuid}`) و حذف (`ConfirmDialog``DELETE`). بعد از هر عملیات `invalidate()`.
4. **تاریخچه تغییرات**: یک مودال/بخش که `GET /session/{uuid}/audit-log` را می‌خواند و هر رکورد را نشان می‌دهد: نوع عملیات (ایجاد/ویرایش/حذف — رنگ‌بندی)، فیلد، مقدار قبل → بعد، کاربر، تاریخ/زمان شمسی (`formatDateTime`). مرتب نزولی.
## نکات مهم
- **همه‌ی مسیرهای تغییر باید audit بزنند**: ویرایش سرویس، کالا، قیمت ویزیت، مبلغ سرویس‌ها، ویرایش/حذف پرداخت، تخفیف. حتی `applyDiscount`/`applyDiscountRule` موجود را هم به `logSessionChange` مجهز کن (field=`discount`).
- تاریخ‌ها Unix؛ پول ریال (ذخیره) / تومان (UI). لیست‌های admin array hydration.
- Entity جدید + ستون‌ها → migration (diff سپس migrate؛ خطوط drift نامرتبط را از migration پاک کن).
- سازگاری مالی: بعد از هر ویرایش/حذف پرداخت، `paid_total`/`remaining`/`is_paid`/`paid_at` باید درست بمانند (بازمحاسبه‌ی فیلدهای کش‌شده).
- **wallet**: تصمیم امن = مسدودکردن ویرایش/حذف پرداخت `wallet` مگر جبران کیف پول پیاده شود.
- مستندات: `docs/api/patient.md` — endpointهای جدید (services edit، payment PATCH/DELETE، audit-log GET) با method/path/permission/body/response/errors.
- این فیچر بزرگ و حساس مالی است — هر وظیفه (۱..۴) جدا پیاده، تست (شامل مسیر خطا/مرزی) و کامیت شود. Backend اول. بعد از کد `graphify update .` (بعد کامیت).