*/ private const BOOKING_LINE_STATUS_COLUMNS = [ 'booking_status' => '%s', 'payment_status' => '%s', 'reserves_capacity' => '%d', ]; private Db $db; public function __construct() { // Database $this->db = new Db(); } /** * @param WP_Post|int $id * @return int[] */ public function insert($id): array { // Get Booking by ID $post = get_post($id); // Invalid Booking if (!($post instanceof WP_Post)) return []; $ids = []; foreach ($this->linePayloads($post, (new BookingEntity($post->ID))->getData()) as $payload) { $exists = $this->existingLineId($post->ID, (int) $payload['option_id'], (string) $payload['session_starts_at']); if ($exists) continue; $inserted = $this->db->insert('daytidy_booking_lines', $payload, $this->linePayloadFormats()); if ($inserted) $ids[] = $this->db->insertId(); } return $ids; } public function replaceWithDraft(int $booking_id, Draft $draft, ?bool $reserve_capacity = null): bool { $post = get_post($booking_id); if (!($post instanceof WP_Post)) return false; $payloads = $this->linePayloads($post, $draft, $reserve_capacity); if (!count($payloads)) return false; $keep_ids = []; foreach ($payloads as $payload) { $line_id = $this->existingLineId($post->ID, (int) $payload['option_id'], (string) $payload['session_starts_at']); if ($line_id > 0) { $updated = $this->db->update( 'daytidy_booking_lines', $payload, ['id' => $line_id], $this->linePayloadFormats(), ['%d'] ); if ($updated === false) return false; $keep_ids[] = $line_id; continue; } $inserted = $this->db->insert('daytidy_booking_lines', $payload, $this->linePayloadFormats()); if (!$inserted) return false; $keep_ids[] = $this->db->insertId(); } $this->deleteExcept($post->ID, $keep_ids); return true; } /** * @param WP_Post|int $booking * @return mixed[] */ public function update($booking): array { // Delete $this->delete($booking); return $this->insert($booking); } /** * @param WP_Post|int $booking */ public function delete($booking): void { // Get Booking by ID $booking = get_post($booking); // Invalid Booking if (!($booking instanceof WP_Post)) return; $this->db->delete('daytidy_booking_lines', [ 'booking_post_id' => $booking->ID, ], [ '%d', ]); } /** * @param WP_Post|int $booking * @return mixed */ public function confirm($booking) { return $this->set($booking, $this->lineState(BookingStatus::CONFIRMED)); } /** * @param WP_Post|int $booking * @return mixed */ public function pending($booking) { return $this->set($booking, $this->lineState(BookingStatus::PENDING)); } /** * @param WP_Post|int $booking * @return mixed */ public function decline($booking) { return $this->set($booking, $this->lineState(BookingStatus::DECLINED)); } /** * @param WP_Post|int $booking * @return mixed */ public function cancel($booking) { return $this->set($booking, $this->lineState(BookingStatus::CANCELLED)); } /** * @param WP_Post|int $booking * @return mixed */ public function complete($booking) { return $this->set($booking, $this->lineState(BookingStatus::COMPLETED)); } /** * @param WP_Post|int $booking * @return mixed */ public function expire($booking) { return $this->set($booking, $this->lineState(BookingStatus::EXPIRED)); } /** * @param WP_Post|int $booking * @return mixed */ public function fail($booking) { return $this->set($booking, $this->lineState(BookingStatus::FAILED)); } /** * @param WP_Post|int $booking * @return mixed */ public function activate($booking) { $booking = get_post($booking); if (!($booking instanceof WP_Post)) return []; return $this->set($booking, $this->lineState(BookingStatus::PENDING)); } /** * @param WP_Post|int $booking * @return mixed */ public function payment($booking, string $status) { return $this->set($booking, [ 'payment_status' => $status, ]); } /** * @param WP_Post|int $booking * @param mixed[] $values * @return mixed */ private function set($booking, array $values) { // Get Booking by ID $booking = get_post($booking); // Invalid Booking if (!($booking instanceof WP_Post)) return []; $data = []; $formats = []; foreach ($values as $key => $value) { if (!isset(self::BOOKING_LINE_STATUS_COLUMNS[$key])) continue; $data[$key] = $key === 'reserves_capacity' ? (int) $value : (string) $value; $formats[] = self::BOOKING_LINE_STATUS_COLUMNS[$key]; } // Nothing to update if (!count($data)) return false; return $this->db->update('daytidy_booking_lines', $data, [ 'booking_post_id' => (int) $booking->ID, ], $formats, [ '%d', ]); } /** * @return array{booking_status: string, reserves_capacity: int} */ private function lineState(string $status): array { return [ 'booking_status' => $status, 'reserves_capacity' => BookingStatus::reservesCapacity($status) ? 1 : 0, ]; } /** * @param int $offering_id * @param int $option_id * @param string $session_starts_at * @return mixed[] */ public function all( int $offering_id, int $option_id, string $session_starts_at, ?OfferingOption $option = null, int $exclude_booking_id = 0 ): array { $line_session_starts_at = $this->sessionStartsAt($option, $session_starts_at, new DateTimeZone('UTC')); $where = ''; $args = [ $offering_id, $option_id, $line_session_starts_at, ]; $exclude_booking_id = absint($exclude_booking_id); if ($exclude_booking_id > 0) { $where = 'AND `booking_post_id` <> %d'; $args[] = $exclude_booking_id; } return $this->db->getResults($this->db->prepare( "SELECT * FROM `#__daytidy_booking_lines` WHERE `offering_id` = %d AND `option_id` = %d AND `status` IN ('publish', 'pending', 'draft', 'future', 'private') AND `reserves_capacity` = 1 AND `session_starts_at` = %s $where", $args )); } /** * @param mixed[] $options * @return array */ private function selectedOptions(array $options): array { $selected = []; foreach ($options as $option_id => $quantity) { $option_id = absint($option_id); $quantity = absint($quantity); if ($option_id > 0 && $quantity > 0) $selected[$option_id] = $quantity; } return $selected; } /** * @return array */ private function linePayloads(WP_Post $post, Draft $data, ?bool $reserve_capacity = null): array { $booking = new BookingEntity($post->ID); $offering = $booking->offering(); if (!$offering) return []; $customer = (new Customer())->booking($booking->id()); $customer_id = $customer ? $customer->ID : 0; $state = (new StateService())->state($booking->id()); $booking_status = $state[StateService::DIMENSION_BOOKING]; $payment_status = $state[StateService::DIMENSION_PAYMENT]; $reserves_capacity = ($reserve_capacity ?? BookingStatus::reservesCapacity($booking_status)) ? 1 : 0; $options = $this->selectedOptions($data->getOptions()); if (!count($options)) return []; $payloads = []; $utc = new DateTimeZone('UTC'); foreach ($options as $option_id => $quantity) { foreach ($data->sessionsForOption($option_id) as $session_starts_at) { if (!strtotime($session_starts_at)) continue; $option = $offering->getOptions()->get($option_id); $resource_id = $data->getResourceIdForOption($option_id); $payloads[] = [ 'booking_post_id' => $booking->id(), 'customer_id' => $customer_id, 'offering_id' => $offering->id(), 'session_starts_at' => $this->sessionStartsAt($option, $session_starts_at, $utc), 'session_ends_at' => $this->sessionEndsAt($offering, $option, $session_starts_at, $utc), 'session_visible_starts_at' => $this->sessionVisibleStartsAt($session_starts_at, $utc), 'session_visible_ends_at' => $this->sessionVisibleEndsAt($offering, $option, $session_starts_at, $utc), 'option_id' => $option_id, 'resource_id' => $resource_id, 'quantity' => $quantity, 'capacity_units' => $this->capacityUnits($option, $quantity), 'resource_units' => $this->resourceUnits($option, $quantity, $resource_id), 'status' => $post->post_status, 'booking_status' => $booking_status, 'payment_status' => $payment_status, 'reserves_capacity' => $reserves_capacity, ]; } } return $payloads; } /** * @return string[] */ private function linePayloadFormats(): array { return [ '%d', '%d', '%d', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%d', '%d', '%s', '%s', '%s', '%d', ]; } private function existingLineId(int $booking_id, int $option_id, string $session_starts_at): int { return absint($this->db->getVar($this->db->prepare( "SELECT `id` FROM `#__daytidy_booking_lines` WHERE `booking_post_id` = %d AND `session_starts_at` = %s AND `option_id` = %d LIMIT 1", $booking_id, $session_starts_at, $option_id ))); } /** * @param int[] $keep_ids */ private function deleteExcept(int $booking_id, array $keep_ids): void { $keep_ids = array_values(array_unique(array_filter(array_map('absint', $keep_ids)))); if (!count($keep_ids)) { $this->delete($booking_id); return; } $placeholders = implode(',', array_fill(0, count($keep_ids), '%d')); $this->db->query($this->db->prepare( "DELETE FROM `#__daytidy_booking_lines` WHERE `booking_post_id` = %d AND `id` NOT IN ($placeholders)", array_merge([$booking_id], $keep_ids) )); } private function capacityUnits(?OfferingOption $option, int $quantity): int { $seats = $option ? $option->seats() : 1; return max(1, $quantity) * max(1, $seats); } private function resourceUnits(?OfferingOption $option, int $quantity, ?int $resource_id): int { if (!$resource_id || !$option || !$option->requiresResource()) return 0; return $option->usesResourceCapacity() ? max(1, $quantity) : 1; } private function sessionStartsAt( ?OfferingOption $option, string $session_starts_at, DateTimeZone $utc ): string { $start = new DateTimeImmutable($session_starts_at, $utc); if ($option && $option->bufferBeforeMinutes() > 0) { $start = $start->sub(new DateInterval('PT' . $option->bufferBeforeMinutes() . 'M')); } return $start->format(DateTimeFormat::tech()); } private function sessionEndsAt( AdventInterface $offering, ?OfferingOption $option, string $session_starts_at, DateTimeZone $utc ): string { $start = new DateTimeImmutable($session_starts_at, $utc); $end = null; if ($option && $option->durationMinutes() > 0) { $end = $start->add(new DateInterval('PT' . $option->durationMinutes() . 'M')); } else { $session = Session::getByTimestamp($session_starts_at, $offering); if ($session) $end = new DateTimeImmutable($session->endDateTime(DateTimeFormat::tech(), $utc), $utc); } if (!$end) { $end = $start->add($offering->getInterval()); } if ($option && $option->bufferAfterMinutes() > 0) { $end = $end->add(new DateInterval('PT' . $option->bufferAfterMinutes() . 'M')); } if ($end <= $start) { $end = $start->add(new DateInterval('PT1M')); } return $end->format(DateTimeFormat::tech()); } private function sessionVisibleStartsAt(string $session_starts_at, DateTimeZone $utc): string { return (new DateTimeImmutable($session_starts_at, $utc))->format(DateTimeFormat::tech()); } private function sessionVisibleEndsAt( AdventInterface $offering, ?OfferingOption $option, string $session_starts_at, DateTimeZone $utc ): string { $start = new DateTimeImmutable($session_starts_at, $utc); $end = null; if ($option && $option->durationMinutes() > 0) { $end = $start->add(new DateInterval('PT' . $option->durationMinutes() . 'M')); } else { $session = Session::getByTimestamp($session_starts_at, $offering); if ($session) $end = new DateTimeImmutable($session->endDateTime(DateTimeFormat::tech(), $utc), $utc); } if (!$end) { $end = $start->add($offering->getInterval()); } if ($end <= $start) { $end = $start->add(new DateInterval('PT1M')); } return $end->format(DateTimeFormat::tech()); } }