refactor: normalize date handling to Tehran timezone

- Updated date handling in BlogSeoFields and ScheduleSection to use Tehran timezone utilities for consistency.
- Introduced `toTehranClockTime`, `tehranWallClockToUnix`, and `todayIso` functions for accurate date representation.
- Modified various components to utilize these new utilities, ensuring that date strings are correctly formatted and timestamps are accurately converted.
- Enhanced API documentation to clarify the handling of date fields, emphasizing the importance of server-local midnight.
- Added tests to verify that date overrides and holidays maintain the correct day without shifting due to timezone discrepancies.
This commit is contained in:
hamed
2026-07-27 19:37:44 +03:30
parent 2963e2ac74
commit b423a0ae4d
21 changed files with 267 additions and 57 deletions
@@ -42,6 +42,21 @@ class AppointmentSettingsController extends BaseController
private readonly \App\Secretary\Security\SecretaryAccessChecker $secretaryAccess,
) {}
/**
* تاریخِ روزمحور (`Y-m-d`) را به نیمه‌شبِ همان روز در تایم‌زون سرور تبدیل می‌کند.
* بدون این نرمال‌سازی، ورودی‌هایی که ساعت دارند (مثلاً ISO با `Z`) روز را جابه‌جا
* می‌کنند. `null` یعنی ورودی نامعتبر.
*/
private function parseDayTimestamp(string $value): ?int
{
$ts = strtotime(trim($value));
if ($ts === false) {
return null;
}
return (int) strtotime(date('Y-m-d', $ts) . ' 00:00:00');
}
/**
* نوع نوبت‌دهی پس از اولین ثبت غیرقابل‌تغییر است — اما فقط داخل همان context.
* پزشکی که در مطب شخصی نوبت‌دهی اسلاتی دارد، همچنان می‌تواند در کلینیک سرویسی
@@ -289,8 +304,8 @@ class AppointmentSettingsController extends BaseController
return $err;
}
$timestamp = strtotime($dateStr);
if ($timestamp === false || $timestamp === -1) {
$timestamp = $this->parseDayTimestamp($dateStr);
if ($timestamp === null) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'فرمت تاریخ نادرست است', 422, 'date');
}
@@ -320,8 +335,8 @@ class AppointmentSettingsController extends BaseController
if (array_key_exists('reason', $data)) $override->setReason($data['reason']);
if (array_key_exists('custom_slots', $data)) $override->setSetting($data['custom_slots']);
if (!empty($data['date'])) {
$ts = strtotime($data['date']);
if ($ts !== false) $override->setDate($ts);
$ts = $this->parseDayTimestamp($data['date']);
if ($ts !== null) $override->setDate($ts);
}
$this->overrideRepo->save($override);
@@ -430,10 +445,10 @@ class AppointmentSettingsController extends BaseController
return $this->error(ErrorCodes::ERR_ACCESS_DENIED, 'کلینیک فقط می‌تواند تعطیلی مخصوص خودش را ثبت کند', 403, 'clinic_uuid');
}
$startTs = strtotime($startStr);
$endTs = strtotime($endStr);
$startTs = $this->parseDayTimestamp($startStr);
$endTs = $this->parseDayTimestamp($endStr);
if (!$startTs || !$endTs || $endTs < $startTs) {
if ($startTs === null || $endTs === null || $endTs < $startTs) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'تاریخ نادرست است', 422);
}
@@ -461,12 +476,12 @@ class AppointmentSettingsController extends BaseController
if (array_key_exists('active', $data)) $holiday->setActive((bool) $data['active']);
if (array_key_exists('reason', $data)) $holiday->setReason($data['reason']);
if (!empty($data['start_date'])) {
$ts = strtotime($data['start_date']);
if ($ts) $holiday->setStartDate($ts);
$ts = $this->parseDayTimestamp($data['start_date']);
if ($ts !== null) $holiday->setStartDate($ts);
}
if (!empty($data['end_date'])) {
$ts = strtotime($data['end_date']);
if ($ts) $holiday->setEndDate($ts);
$ts = $this->parseDayTimestamp($data['end_date']);
if ($ts !== null) $holiday->setEndDate($ts);
}
$this->holidayRepo->save($holiday);
+3
View File
@@ -91,6 +91,9 @@ class DateOverride
'clinic_uuid' => $this->clinic?->getUuid(),
'context' => $this->clinic === null ? 'personal' : 'clinic',
'date' => $this->date,
// نمایش تاریخ باید از این رشته خوانده شود؛ تبدیل timestamp در مرورگر
// با UTC انجام می‌شود و برای نیمه‌شبِ تهران یک روز عقب می‌افتد.
'date_string' => date('Y-m-d', $this->date),
'active' => $this->active,
'reason' => $this->reason,
'custom_slots' => $this->setting ?? [],
+4
View File
@@ -90,6 +90,10 @@ class Holiday
'scope' => $this->clinic === null ? 'global' : 'clinic',
'start_date' => $this->startDate,
'end_date' => $this->endDate,
// نمایش تاریخ باید از این رشته‌ها خوانده شود؛ تبدیل timestamp در مرورگر
// با UTC انجام می‌شود و برای نیمه‌شبِ تهران یک روز عقب می‌افتد.
'start_date_string' => date('Y-m-d', $this->startDate),
'end_date_string' => date('Y-m-d', $this->endDate),
'active' => $this->active,
'reason' => $this->reason,
'created_at' => $this->createdAt,