$payload Fields. * * @throws DomainException When validation fails. */ public function handle( array $payload ): Booking { $service_id = (int) ( $payload['service_id'] ?? 0 ); $agent_id = (int) ( $payload['agent_id'] ?? 0 ); $location_id = isset( $payload['location_id'] ) ? (int) $payload['location_id'] : 0; $service = $this->services->find_by_id( $service_id ); if ( null === $service ) { throw new DomainException( 'booking.service_missing', 'Service not found.' ); } if ( $agent_id < 1 ) { throw new DomainException( 'booking.agent_required', 'Agent is required.' ); } $duration = null; foreach ( $this->durations->find_by_service_id( $service_id ) as $row ) { if ( $row->is_default() ) { $duration = $row; break; } if ( null === $duration ) { $duration = $row; } } if ( null === $duration ) { throw new DomainException( 'booking.duration_missing', 'This service has no duration.' ); } $tz_id = new TimeZoneId( (string) $this->settings->get( 'general.timezone' ) ); $tz = new DateTimeZone( $tz_id->to_string() ); $start_raw = trim( (string) ( $payload['start_local'] ?? $payload['start_at'] ?? '' ) ); if ( '' === $start_raw ) { throw new DomainException( 'booking.start_required', 'Start date and time are required.' ); } $start_local = DateTimeImmutable::createFromFormat( 'Y-m-d\TH:i', $start_raw, $tz ); if ( false === $start_local ) { $start_local = DateTimeImmutable::createFromFormat( 'Y-m-d H:i', $start_raw, $tz ); } if ( false === $start_local ) { try { $start_local = new DateTimeImmutable( $start_raw, $tz ); } catch ( \Exception $e ) { throw new DomainException( 'booking.start_invalid', 'Start date and time are invalid.' ); } } $end_local = $start_local->modify( '+' . $duration->duration_min() . ' minutes' ); $utc = new DateTimeZone( 'UTC' ); $start_utc = $start_local->setTimezone( $utc ); $end_utc = $end_local->setTimezone( $utc ); $buffer_start = $start_utc->modify( '-' . $service->buffer_before_min() . ' minutes' ); $buffer_end = $end_utc->modify( '+' . $service->buffer_after_min() . ' minutes' ); $first = trim( (string) ( $payload['customer_first_name'] ?? $payload['first_name'] ?? '' ) ); $last = trim( (string) ( $payload['customer_last_name'] ?? $payload['last_name'] ?? '' ) ); $email = trim( (string) ( $payload['customer_email'] ?? $payload['email'] ?? '' ) ); if ( '' === $first || '' === $email ) { throw new DomainException( 'booking.customer_required', 'Customer first name and email are required.' ); } $phone = null; $phone_raw = trim( (string) ( $payload['customer_phone'] ?? $payload['phone'] ?? '' ) ); if ( '' !== $phone_raw ) { $phone = new PhoneNumber( $phone_raw ); } $command = new CreateBookingCommand( $service_id, $duration->id(), $agent_id, $location_id > 0 ? $location_id : null, $start_utc, $end_utc, $buffer_start, $buffer_end, $tz_id, $start_local, max( 1, (int) ( $payload['attendees'] ?? 1 ) ), $duration->price(), $first, '' !== $last ? $last : null, new EmailAddress( $email ), $phone, isset( $payload['notes'] ) ? trim( (string) $payload['notes'] ) : null, 0, true, ); return $this->create->handle( $command ); } }