diff --git a/assets/admin/pages/AppointmentEditPage.test.tsx b/assets/admin/pages/AppointmentEditPage.test.tsx
index cd2cfa23..c5c8c261 100644
--- a/assets/admin/pages/AppointmentEditPage.test.tsx
+++ b/assets/admin/pages/AppointmentEditPage.test.tsx
@@ -50,6 +50,29 @@ describe('AppointmentEditPage (ویرایش نوبت)', () => {
expect(screen.getByDisplayValue('یادداشت')).toBeInTheDocument();
});
+ // بخش و سرویس هر دو باید برچسب واقعی نشان دهند، نه placeholder. فهرست سرویسها
+ // از بخش میآید، پس بخشِ خالی یعنی سرویسِ ذخیرهشده هم انتخابنشده دیده میشود —
+ // همان باگی که در ۲۰۲۶-۰۸-۰۸ گزارش شد.
+ it('shows the stored section and service, not their placeholders', async () => {
+ renderEdit();
+ await screen.findByText('مشخصات سرویس:');
+
+ expect(await screen.findByText('زیبایی')).toBeInTheDocument();
+ expect(await screen.findByText('لیزر')).toBeInTheDocument();
+ expect(screen.queryByText('انتخاب بخش')).not.toBeInTheDocument();
+ expect(screen.queryByText('انتخاب سرویس')).not.toBeInTheDocument();
+ });
+
+ // بلوک بیعانه فعلاً از فرم برداشته شده، ولی مقدارش همچنان ذخیره میشود
+ // (اثباتش در تست بعدی: deposit_required در payload هست).
+ it('no longer renders the deposit block', async () => {
+ renderEdit();
+ await screen.findByText('مشخصات سرویس:');
+
+ expect(screen.queryByText('بیعانه:')).not.toBeInTheDocument();
+ expect(screen.queryByText('بیعانه مورد نیاز است.')).not.toBeInTheDocument();
+ });
+
it('patches the general update endpoint with the edited values', async () => {
renderEdit();
await screen.findByText('مشخصات سرویس:');
diff --git a/assets/admin/pages/AppointmentEditPage.tsx b/assets/admin/pages/AppointmentEditPage.tsx
index 6619865f..8cda2210 100644
--- a/assets/admin/pages/AppointmentEditPage.tsx
+++ b/assets/admin/pages/AppointmentEditPage.tsx
@@ -6,9 +6,7 @@ import { toast } from 'sonner';
import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import PersianDateInput from '../components/ui/PersianDateInput';
-import PriceInput from '../components/ui/PriceInput';
import SearchableSelect from '../components/ui/SearchableSelect';
-import { WalletChargeLink } from '../components/AppointmentActions';
import { formatRial, rialToToman, tomanToRial } from '../lib/utils';
import { DEFAULT_SERVICE_CATEGORY } from '../lib/insuranceShares';
import BackButton from '../components/ui/BackButton';
@@ -16,7 +14,6 @@ import { useAppointmentInsurance } from '../hooks/useAppointmentInsurance';
import { useDoctorBookingServices } from '../hooks/useDoctorBookingServices';
import ServiceSlotPicker from '../components/appointments/ServiceSlotPicker';
import type { PickedService, ServicePick } from '../components/appointments/ServiceSlotPicker';
-import Switch from '../components/ui/Switch';
interface Option { uuid: string; name?: string; full_name?: string }
@@ -357,24 +354,12 @@ export default function AppointmentEditPage() {
)}
-
بیعانه:
-
-
- {depositRequired && (
- <>
-
-
-
-
-
- >
- )}
-
+ {/*
+ بخش «بیعانه» فعلاً از فرم برداشته شد (درخواست ۲۰۲۶-۰۸-۰۸).
+ state و ارسالش عمداً سر جایشان ماندند: مقدارِ فعلیِ نوبت هنگام هیدریت
+ خوانده و همان هنگام ذخیره پس فرستاده میشود، پس ویرایشِ بقیهٔ فیلدها
+ بیعانهٔ ثبتشده را پاک نمیکند. برگرداندنِ UI یعنی همین بلوک، نه بیشتر.
+ */}
diff --git a/docs/api/appointment.md b/docs/api/appointment.md
index dabb2611..5fb34508 100644
--- a/docs/api/appointment.md
+++ b/docs/api/appointment.md
@@ -390,6 +390,13 @@ Get appointment detail.
**Permission:** `AUTH` — see [Single-appointment access model](#single-appointment-access-model)
+> **`service_section` اینجا استنتاجی است.** نوبتِ سرویسیِ کلینیک بخش را روی خودِ نوبت
+> ذخیره نمیکند و فقط سرویس را نگه میدارد. این اندپوینت — و **فقط** این اندپوینت — وقتی
+> `service_section` خالی باشد آن را از `service_item` درمیآورد. حدس نیست:
+> `service_items.section_id` ستونی `NOT NULL` است، پس هر سرویسی دقیقاً یک بخش دارد.
+> نوبتِ بدون هیچ سرویسی همچنان `null` میگیرد. فهرستها (`GET /api/v1/appointments…`)
+> عمداً این استنتاج را ندارند و رفتارشان تغییر نکرده.
+
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
diff --git a/src/Appointment/Controller/AppointmentController.php b/src/Appointment/Controller/AppointmentController.php
index 990e1f39..b9a9fc79 100644
--- a/src/Appointment/Controller/AppointmentController.php
+++ b/src/Appointment/Controller/AppointmentController.php
@@ -704,7 +704,27 @@ class AppointmentController extends BaseController
return $this->error(ErrorCodes::ERR_ACCESS_DENIED, 'دسترسی ممنوع', 403);
}
- return $this->success(['data' => $appointment->toArray()]);
+ $payload = $appointment->toArray();
+
+ /**
+ * نوبتِ سرویسیِ کلینیک `service_section_id` را روی خودِ نوبت نمینویسد؛ فقط
+ * سرویس را نگه میدارد. ولی فرم ویرایش فهرستِ سرویسها را از **بخش**
+ * میگیرد، پس بخشِ خالی یعنی فهرست سرویس هم خالی میماند و سرویسِ
+ * ذخیرهشده — با اینکه مقدارش درست است — بیبرچسب و انگار انتخابنشده
+ * دیده میشود.
+ *
+ * بخش از خودِ سرویس درمیآید و حدس نیست: `service_items.section_id` ستونِ
+ * NOT NULL است، پس هر سرویسی دقیقاً یک بخش دارد.
+ *
+ * عمداً اینجاست و نه در `Appointment::toArray()`: آنجا فهرستِ نوبتها هم
+ * از همین متد میخواند و نباید ناگهان بخشی نشان دهد که تا امروز نداشت.
+ */
+ if ($payload['service_section'] === null && ($item = $appointment->getServiceItem()) !== null) {
+ $section = $item->getSection();
+ $payload['service_section'] = ['uuid' => $section->getUuid(), 'name' => $section->getName()];
+ }
+
+ return $this->success(['data' => $payload]);
}
#[OA\Get(
diff --git a/tests/Appointment/AppointmentDetailSectionFallbackTest.php b/tests/Appointment/AppointmentDetailSectionFallbackTest.php
new file mode 100644
index 00000000..a894b2b8
--- /dev/null
+++ b/tests/Appointment/AppointmentDetailSectionFallbackTest.php
@@ -0,0 +1,87 @@
+createUser(['ROLE_DOCTOR']);
+ $doctor = new Doctor($owner, 'دکتر تست بخش');
+ $this->em->persist($doctor);
+ // بخش به شناسهٔ پزشک نیاز دارد، پس پزشک باید پیش از ساختش flush شود.
+ $this->em->flush();
+
+ $section = new ServiceSection('doctor', $doctor->getId(), 'بخش لیزر');
+ $this->em->persist($section);
+
+ $item = new ServiceItem($section, 'لیزر توتال', 0);
+ $item->setDurationMinutes(40)->setBookable(true);
+ $this->em->persist($item);
+ $this->em->flush();
+
+ return [$doctor, $section, $item];
+ }
+
+ /** ✅ موفق — بخشِ ذخیرهنشده از روی سرویس پر میشود. */
+ public function testSectionIsDerivedFromTheServiceItemWhenMissing(): void
+ {
+ [$doctor, $section, $item] = $this->doctorWithOneService();
+
+ $patient = $this->createUser(['ROLE_USER']);
+ $start = strtotime('+5 days 10:00');
+ $appt = $this->newAppointment($doctor, $patient, $start, $start + 2400);
+
+ $appt->replaceServiceItems([$item]);
+ self::assertNull($appt->getServiceSection(), 'پیشفرضِ این سناریو: نوبت بخش ندارد');
+ $this->em->persist($appt);
+ $this->em->flush();
+
+ $res = $this->authJson('GET', '/api/v1/appointment/' . $appt->getUuid(), $doctor->getUser());
+ self::assertSame(200, $this->responseCode());
+
+ $payload = $res['data']['data'];
+ self::assertSame($section->getUuid(), $payload['service_section']['uuid']);
+ self::assertSame('بخش لیزر', $payload['service_section']['name']);
+ self::assertSame($item->getUuid(), $payload['service_item']['uuid']);
+ }
+
+ /** ⚠️ مرزی — نوبتِ بدون هیچ سرویسی چیزی برای استنتاج ندارد و `null` میماند. */
+ public function testSectionStaysNullWhenTheAppointmentHasNoService(): void
+ {
+ [$doctor] = $this->doctorWithOneService();
+
+ $patient = $this->createUser(['ROLE_USER']);
+ $start = strtotime('+6 days 10:00');
+ $appt = $this->newAppointment($doctor, $patient, $start, $start + 1800);
+ $this->em->persist($appt);
+ $this->em->flush();
+
+ $res = $this->authJson('GET', '/api/v1/appointment/' . $appt->getUuid(), $doctor->getUser());
+ self::assertSame(200, $this->responseCode());
+ self::assertNull($res['data']['data']['service_section']);
+ }
+
+ /** ❌ خطا — نوبتِ ناموجود همچنان ۴۰۴ است؛ fallback مسیر خطا را عوض نمیکند. */
+ public function testUnknownAppointmentStillReturns404(): void
+ {
+ [$doctor] = $this->doctorWithOneService();
+
+ $this->authJson('GET', '/api/v1/appointment/00000000-0000-0000-0000-000000000000', $doctor->getUser());
+ self::assertSame(404, $this->responseCode());
+ }
+}