jalali->jalaliToGregorian($jy, $jm, $jd); return (new \DateTimeImmutable( sprintf('%04d-%02d-%02d 00:00:00', $gy, $gm, $gd), new \DateTimeZone(self::NATIONAL_TIMEZONE), ))->getTimestamp(); } /** * یک تعطیل رسمی را ثبت یا به‌روز می‌کند. idempotent: تکیه‌گاهش `date` است که * یکتاست، پس import دوباره ردیف تکراری نمی‌سازد. * * خودش flush می‌کند و این عمدی است: سپردنِ flush به فراخوان یعنی اگر او * EntityManager دیگری در دست داشته باشد، persist روی یکی و flush روی دیگری * می‌افتد و **هیچ ردیفی نوشته نمی‌شود، بی‌هیچ خطایی**. همین در تست‌ها اتفاق افتاد، * چون هر درخواست HTTP کرنل را از نو می‌سازد و سرویس از کانتینر تازه می‌آید. */ public function upsertNational(int $jy, int $jm, int $jd, string $title): NationalHoliday { $date = $this->jalaliToMidnight($jy, $jm, $jd); $existing = $this->holidays->findByDate($date); if ($existing !== null) { $existing->setTitle($title); $this->em->flush(); return $existing; } $holiday = new NationalHoliday( $date, sprintf('%04d-%02d-%02d', $jy, $jm, $jd), $jy, $title, ); $this->em->persist($holiday); $this->em->flush(); return $holiday; } /** * همهٔ تعطیلات یک سال را پاک می‌کند — تنها راه تصحیحِ ردیفی که با تاریخ غلط * نوشته شده، چون upsert روی `date` کلید می‌خورد و ردیف غلط را نمی‌شناسد. */ public function purgeYear(int $jalaliYear): int { return (int) $this->em->createQuery( 'DELETE FROM App\Resource\Entity\NationalHoliday h WHERE h.jalaliYear = :year' )->setParameter('year', $jalaliYear)->execute(); } /** @return NationalHoliday[] */ public function forYear(int $jalaliYear): array { return $this->holidays->findForYear($jalaliYear); } /** * استثنای محیط. روی همان تاریخ دوباره فرستادن، همان ردیف را عوض می‌کند — وگرنه * کلید یکتا با خطای خام دیتابیس می‌شکست. */ public function setOverride(string $entityType, int $entityId, int $date, bool $isWorking, ?string $note): TenantHolidayOverride { $midnight = $this->midnightTehran($date); $existing = $this->overrides->findForDate($entityType, $entityId, $midnight); if ($existing !== null) { $existing->setWorking($isWorking)->setNote($note); $this->em->flush(); return $existing; } $override = new TenantHolidayOverride($entityType, $entityId, $midnight, $isWorking); $override->setNote($note); $this->em->persist($override); $this->em->flush(); return $override; } public function deleteOverride(TenantHolidayOverride $override): void { $this->em->remove($override); $this->em->flush(); } /** @return TenantHolidayOverride[] */ public function overridesForYear(string $entityType, int $entityId, int $jalaliYear): array { $from = $this->jalaliToMidnight($jalaliYear, 1, 1); $to = $this->jalaliToMidnight($jalaliYear + 1, 1, 1); return array_values($this->overrides->mapForRange($entityType, $entityId, $from, $to)); } public function assertJalaliYear(mixed $value): int { if (!is_numeric($value) || (int) $value < 1300 || (int) $value > 1500) { throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'سال شمسی نامعتبر است', 422, 'year'); } return (int) $value; } private function midnightTehran(int $timestamp): int { return (new \DateTimeImmutable('@' . $timestamp)) ->setTimezone(new \DateTimeZone(self::NATIONAL_TIMEZONE)) ->setTime(0, 0) ->getTimestamp(); } }