feat(booking): add service-aware reschedule endpoint
POST /api/v1/appointment/{uuid}/service-reschedule takes only a start time and
derives the length from the appointment's services. PATCH also validates the
duration, but the client must already know the correct slot_end; not needing that
knowledge is what lets the edit form drop its manual time inputs.
The start must be a member of getServiceStartTimes(), not merely free:
isSlotTaken() reports collisions with other appointments, while the offered list
also applies shift bounds, holidays, date overrides, the booking window and the
buffer. Without it a secretary could park an appointment at 3am.
forManagement comes from canManageContext(), not canManage(): a patient moving
their own appointment must still respect the public booking window.
Task: docs/new_feture/taskes/task-00-service-mode-completion/
Slot-mode contract: unchanged (--group=slot-mode-frozen green)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,7 @@ class AppointmentController extends BaseController
|
||||
private readonly \App\Appointment\Security\AppointmentAccessChecker $accessChecker,
|
||||
private readonly \App\Appointment\Service\AppointmentInsuranceService $appointmentInsurance,
|
||||
private readonly \App\Appointment\Service\ServiceBookingCalculator $serviceCalculator,
|
||||
private readonly \App\Appointment\Service\ServiceRescheduleService $rescheduleService,
|
||||
private readonly \Psr\Log\LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
@@ -1223,6 +1224,62 @@ class AppointmentController extends BaseController
|
||||
return $this->success(['data' => $appointment->toArray()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* جابهجایی سرویسآگاه — فقط حالت نوبتدهی سرویسی.
|
||||
*
|
||||
* کلاینت مدت نمیفرستد: `start` میدهد و سرور مدت را از سرویسهای نوبت (یا فهرست
|
||||
* صریحِ درخواست) حساب میکند. این تفاوت با `PATCH` است که کلاینت باید `slot_end`
|
||||
* درست را از قبل بداند.
|
||||
*
|
||||
* POST /api/v1/appointment/{uuid}/service-reschedule
|
||||
*/
|
||||
#[Route('/api/v1/appointment/{uuid}/service-reschedule', methods: ['POST'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function serviceReschedule(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$appointment = $this->appointmentRepo->findByUuid($uuid);
|
||||
if ($appointment === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'نوبت یافت نشد', 404);
|
||||
}
|
||||
if (!$this->canManage($appointment, $user)) {
|
||||
return $this->error(ErrorCodes::ERR_ACCESS_DENIED, 'دسترسی ممنوع', 403);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$start = (int) ($data['start'] ?? 0);
|
||||
if ($start <= 0) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'زمان شروع الزامی است', 422, 'start');
|
||||
}
|
||||
|
||||
$serviceUuids = array_key_exists('service_item_uuids', $data)
|
||||
? array_values(array_filter(array_map('trim', (array) $data['service_item_uuids'])))
|
||||
: null;
|
||||
|
||||
try {
|
||||
$duration = $this->rescheduleService->reschedule(
|
||||
$appointment,
|
||||
$start,
|
||||
$serviceUuids,
|
||||
(array) ($data['durations'] ?? []),
|
||||
$this->accessChecker->canManageContext($user, $appointment->getDoctor(), $appointment->getClinic()),
|
||||
array_key_exists('version', $data) ? (int) $data['version'] : null,
|
||||
);
|
||||
} catch (OptimisticLockException) {
|
||||
return $this->error(ErrorCodes::ERR_CONFLICT_001, 'تداخل ویرایش همزمان. لطفاً دوباره تلاش کنید', 409);
|
||||
} catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException) {
|
||||
return $this->error(ErrorCodes::ERR_CONFLICT_001, 'این بازه زمانی قبلاً رزرو شده است', 409, 'start');
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'uuid' => $appointment->getUuid(),
|
||||
'slot_start' => $appointment->getSlotStart(),
|
||||
'slot_end' => $appointment->getSlotEnd(),
|
||||
'total_duration_minutes' => $duration->totalMinutes,
|
||||
'buffer_minutes' => $duration->bufferMinutes,
|
||||
'warnings' => $duration->warnings,
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Timeline: رویدادهای یک نوبت ───────────────────────────────────────────
|
||||
|
||||
#[Route('/api/v1/appointment/{uuid}/events', methods: ['GET'])]
|
||||
|
||||
Reference in New Issue
Block a user