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

12 KiB
Raw Blame History

ویرایش سرویس‌های مراجعه + ویرایش/حذف پرداخت + 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:

// 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}) و حذف (ConfirmDialogDELETE). بعد از هر عملیات 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 . (بعد کامیت).