12 KiB
ویرایش سرویسهای مراجعه + ویرایش/حذف پرداخت + Audit Log مالی جامع
پروژه
clinicpro (backend Symfony + پنل ادمین React). تک-ریپو.
تست: پنل ادمین با
09390039833 / 09390039833. اجرا داخل ddev. قرارداد پول: ذخیره/API ریال، UI تومان (tomanToRial/rialToToman). تاریخ Unix.
زمینه
مراجعه (PatientSession) فقط ایجاد میشود؛ پس از ثبت، امکان ویرایش سرویسها/کالاها/قیمت ویزیت/بیمه وجود ندارد و پرداختهای ثبتشده نه ویرایش میشوند نه حذف. کاربر میخواهد بتواند همهی اینها را ویرایش کند، اما هر تغییر مالی/خدماتی باید در یک Audit Log ثبت و قابلمشاهده باشد (چه کسی، چه چیزی، کِی، مقدار قبل/بعد، نوع عملیات).
مشکل / هدف
- ویرایش کامل یک مراجعه پس از ثبت: سرویسها (افزودن/حذف/تعداد)، کالاهای مصرفی، قیمت ویزیت، بیمه/درصدها، یادداشت، تاریخ مراجعه — با محاسبهی مجدد
services_total_rials/final_price_rials. - ویرایش و حذف پرداختهای ثبتشده، با نگهداشتن سازگاری
paid_total/remaining/payment_method/paid_at. - 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(ManyToOnePatientSession,onDelete: CASCADE),fieldstring (مثلvisit_price_rials,services,consumables,payment,discount),operationstring (create|update|delete),old_valuetext nullable,new_valuetext nullable,actor_user_idint nullable,actor_namestring nullable,notestring nullable,created_atint. - constructor
(PatientSession $session, string $field, string $operation)؛ fluentsetActor(?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 — فرم ویرایش + کنترل پرداخت + نمایش تاریخچه
- منوی کارت (
SessionServiceCard.tsxdropdown L101-114): افزودن آیتمهای «ویرایش» و «تاریخچه تغییرات» (props جدیدonEdit(session),onViewAudit(session)). - فرم ویرایش: از
CreateStepبازاستفاده کن (یا کامپوننت مشترک) در حالت edit؛ با دادهی فعلی session پر شود و بهPATCH /session/{uuid}(با body مثل CreateStep L233-245) بفرستد. مسیرsession/{uuid}/editیا مودال. - ردیف پرداخت (
PaymentStep.tsxL250-270): برای هر پرداخت آیکون ویرایش (مودال کوچک: مبلغ تومان + روش + تاریخ →PATCH .../payments/{uuid}) و حذف (ConfirmDialog→DELETE). بعد از هر عملیاتinvalidate(). - تاریخچه تغییرات: یک مودال/بخش که
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 .(بعد کامیت).