feat: Implement SMS sending functionality with KavehNegar and Rangineh providers

- Add SendSmsMessage class for encapsulating SMS message data.
- Create KavehNegarProvider and RanginehProvider classes implementing SmsProviderInterface for sending SMS.
- Implement SmsLogRepository and SmsTemplateRepository for managing SMS logs and templates.
- Develop SendSmsHandler for handling SMS sending messages.
- Create SmsService to manage SMS dispatching and logging.
- Add UserProfileController for managing user profiles with CRUD operations.
- Implement UserProfile entity and repository for user profile data management.
- Update symfony.lock and bootstrap.php for project dependencies and environment setup.
This commit is contained in:
hamed
2026-06-09 22:00:34 +03:30
commit de1a78a235
222 changed files with 36388 additions and 0 deletions
@@ -0,0 +1,104 @@
<?php
namespace App\Appointment\Service;
use App\Appointment\Entity\Appointment;
use App\Appointment\Repository\AppointmentRepository;
use App\Appointment\Repository\DateOverrideRepository;
use App\Appointment\Repository\HolidayRepository;
use App\Appointment\Repository\WeeklyScheduleRepository;
use App\Doctor\Entity\Doctor;
class SlotCalculatorService
{
private const DAY_MAP = [
0 => 'sunday', 1 => 'monday', 2 => 'tuesday', 3 => 'wednesday',
4 => 'thursday', 5 => 'friday', 6 => 'saturday',
];
public function __construct(
private readonly WeeklyScheduleRepository $scheduleRepo,
private readonly DateOverrideRepository $overrideRepo,
private readonly HolidayRepository $holidayRepo,
private readonly AppointmentRepository $appointmentRepo,
) {}
/**
* Returns available slots for a doctor on a given date.
* @param string $date 'Y-m-d' format
* @return array[] [{start: int, end: int, start_time: string, end_time: string}]
*/
public function getAvailableSlots(Doctor $doctor, string $date): array
{
$dayStart = (int) strtotime($date . ' 00:00:00');
$dayEnd = $dayStart + 86400;
// Check if in holiday
$holidays = $this->holidayRepo->findActiveByDoctor($doctor, $dayStart, $dayEnd - 1);
if (!empty($holidays)) return [];
// Check date override first
$overrides = $this->overrideRepo->findByDoctor($doctor);
foreach ($overrides as $override) {
$overDate = date('Y-m-d', $override->getDate());
if ($overDate === $date) {
if (!$override->isActive()) return [];
return $this->buildSlots($override->getSetting() ?? [], $dayStart);
}
}
// Fall back to weekly schedule
$schedule = $this->scheduleRepo->findByDoctor($doctor);
if ($schedule === null) return [];
$dow = (int) date('w', $dayStart);
$dayName = self::DAY_MAP[$dow];
$setting = $schedule->getSetting();
$dayConfig = $setting[$dayName] ?? null;
if ($dayConfig === null || !($dayConfig['active'] ?? false)) return [];
$rawSlots = $this->buildSlots($dayConfig['slots'] ?? [], $dayStart);
// Filter out already-booked slots
return $this->filterBookedSlots($doctor, $rawSlots);
}
/** @return array[] */
private function buildSlots(array $slotConfigs, int $dayStart): array
{
$slots = [];
foreach ($slotConfigs as $config) {
$startSec = $this->parseTime($config['start'] ?? '00:00');
$endSec = $this->parseTime($config['end'] ?? '00:00');
$duration = (int) ($config['duration'] ?? 30) * 60;
if ($duration <= 0 || $endSec <= $startSec) continue;
for ($t = $startSec; $t + $duration <= $endSec; $t += $duration) {
$slotStart = $dayStart + $t;
$slotEnd = $slotStart + $duration;
$slots[] = [
'start' => $slotStart,
'end' => $slotEnd,
'start_time' => gmdate('H:i', $t),
'end_time' => gmdate('H:i', $t + $duration),
];
}
}
return $slots;
}
private function filterBookedSlots(Doctor $doctor, array $slots): array
{
return array_values(array_filter($slots, function (array $slot) use ($doctor): bool {
return !$this->appointmentRepo->isSlotTaken($doctor, $slot['start'], $slot['end']);
}));
}
private function parseTime(string $time): int
{
[$h, $m] = explode(':', $time, 2) + [0, 0];
return ((int)$h * 3600) + ((int)$m * 60);
}
}