PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.64
Timetics – Appointment Booking Calendar & Scheduling v1.0.64
1.0.64 1.0.62 1.0.63 1.0.61 1.0.60 1.0.59 1.0.58 1.0.57 1.0.56 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.2 1.0.20 1.0.21 All 65 releases
← All changes | core/bookings/booking.php +135 -26 1.0.61 → 1.0.64 View file →
@@ -6,8 +6,10 @@
6 6 */
7 7
8 8 namespace Timetics\Core\Bookings;
9 9
10 +defined( 'ABSPATH' ) || exit;
11 +
10 12 use Timetics\Core\Appointments\Appointment;
11 13 use Timetics\Core\Customers\Customer;
12 14 use Timetics\Core\Integrations\Google\Service\Calendar;
13 15 use Timetics\Core\Staffs\Staff;
@@ -661,18 +663,25 @@
661 663
662 664 /**
663 665 * Find the Booking_Entry (shared per-slot schedule record) this booking occupies.
664 666 *
665 - * @param Appointment|null $meeting Optional pre-built meeting for this booking.
666 - * @return Booking_Entry|null The first matching entry, or null if none.
667 + * @param Appointment|null $meeting Optional pre-built meeting for this booking.
668 + * @param string $start_date Slot date. Defaults to the booking's own.
669 + * @param string $start_time Slot start. Defaults to the booking's own.
670 + * @param string $timezone Timezone of the two above. Defaults to the booking's own.
671 + * @return Booking_Entry|null The first matching entry, or null if none.
667 672 */
668 - private function find_slot_entry( $meeting = null ) {
673 + private function find_slot_entry( $meeting = null, $start_date = '', $start_time = '', $timezone = '' ) {
669 674 $meeting = $meeting ?: new Appointment( $this->get_appointment() );
670 675 $booking_entry = new Booking_Entry();
671 676
677 + $start_date = $start_date ?: $this->get_start_date();
678 + $start_time = $start_time ?: $this->get_start_time();
679 + $timezone = $timezone ?: $this->get_timezone();
680 +
672 681 $date_time = timetics_convert_timezone(
673 - $this->get_start_date() . ' ' . $this->get_start_time(),
674 - $this->get_timezone(),
682 + $start_date . ' ' . $start_time,
683 + $timezone,
675 684 $meeting->get_timezone()
676 685 );
677 686
678 687 $entries = $booking_entry->find(
@@ -701,38 +710,138 @@
701 710 if ( $this->get_prop( 'slot_released' ) ) {
702 711 return;
703 712 }
704 713
714 + $this->release_slot_at( $this->get_start_date(), $this->get_start_time() );
715 +
716 + update_post_meta( $this->id, $this->meta_prefix . 'slot_released', 1 );
717 + }
718 +
719 + /**
720 + * Take back the slot this booking released.
721 + *
722 + * A declined payment marks the booking failed and releases its slot, so an
723 + * attempt the customer walks away from does not block the time forever. When
724 + * the customer retries on that same booking the slot has to be taken back
725 + * before any money moves, otherwise the booking finalizes while the slot still
726 + * reads as free and the next customer can book straight over it.
727 + *
728 + * @return bool True when the slot is held, false when it is no longer free.
729 + */
730 + public function reserve_slot() {
731 + if ( ! $this->is_booking() ) {
732 + return false;
733 + }
734 +
735 + // Nothing was released, so the slot is still held from booking creation.
736 + if ( ! $this->get_prop( 'slot_released' ) ) {
737 + return true;
738 + }
739 +
705 740 $meeting = new Appointment( $this->get_appointment() );
706 741 $entry = $this->find_slot_entry( $meeting );
742 + $seats = ! empty( $this->get_seat() ) ? (array) $this->get_seat() : [];
743 + $taking = max( 1, count( $seats ) );
707 744
708 745 if ( $entry ) {
709 - if ( 'one-to-one' === strtolower( $meeting->get_type() ) ) {
710 - $entry->delete();
711 - } else {
712 - $booked = max( 0, intval( $entry->get_booked() ) - 1 );
713 - $booked_seat = ! empty( $this->get_seat() ) ? $this->get_seat() : [];
714 - $existing_seat = ! empty( $entry->get_seats() ) ? $entry->get_seats() : [];
746 + $booked = intval( $entry->get_booked() ) + $taking;
715 747
716 - $entry->update(
717 - [
718 - 'booked' => $booked,
719 - 'seats' => array_values( array_diff( $existing_seat, $booked_seat ) ),
720 - ]
748 + // Someone else took the time while this booking was in the failed state.
749 + if ( $booked > $meeting->get_effective_capacity() ) {
750 + return false;
751 + }
752 +
753 + $existing = ! empty( $entry->get_seats() ) ? (array) $entry->get_seats() : [];
754 +
755 + $entry->update(
756 + [
757 + 'booked' => $booked,
758 + 'seats' => array_values( array_unique( array_merge( $existing, $seats ) ) ),
759 + ]
760 + );
761 + } else {
762 + // A one-to-one release deletes the entry outright, so recreate it. Entries
763 + // are stored in the meeting's timezone, the booking's in the customer's.
764 + $start = timetics_convert_timezone( $this->get_start_date() . ' ' . $this->get_start_time(), $this->get_timezone(), $meeting->get_timezone() );
765 + $end = timetics_convert_timezone( $this->get_start_date() . ' ' . $this->get_end_time(), $this->get_timezone(), $meeting->get_timezone() );
766 +
767 + $booking_entry = new Booking_Entry();
768 +
769 + $booking_entry->create(
770 + [
771 + 'meeting_id' => $meeting->get_id(),
772 + 'staff_id' => $this->get_staff_id(),
773 + 'customer_id' => $this->get_customer(),
774 + 'booking_id' => $this->id,
775 + 'booked' => $taking,
776 + 'date' => $start->format( 'Y-m-d' ),
777 + 'start' => $start->format( 'h:i a' ),
778 + 'end' => $end->format( 'h:i a' ),
779 + 'seats' => $seats,
780 + ]
781 + );
782 + }
783 +
784 + delete_post_meta( $this->id, $this->meta_prefix . 'slot_released' );
785 +
786 + return true;
787 + }
788 +
789 + /**
790 + * Release the entry for one named slot of this booking.
791 + *
792 + * Rescheduling saves the new time first, so the old slot has to be named
793 + * rather than read off the booking. No idempotency guard: the booking lives
794 + * on and may release more slots as it moves.
795 + *
796 + * @param string $start_date Slot date, Y-m-d.
797 + * @param string $start_time Slot start.
798 + * @param string $timezone Timezone of the two above. Defaults to the booking's own.
799 + *
800 + * @return void
801 + */
802 + public function release_slot_at( $start_date, $start_time, $timezone = '' ) {
803 + if ( ! $this->is_booking() ) {
804 + return;
805 + }
806 +
807 + $meeting = new Appointment( $this->get_appointment() );
808 + $entry = $this->find_slot_entry( $meeting, $start_date, $start_time, $timezone );
809 +
810 + if ( ! $entry ) {
811 + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
812 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug-only diagnostics for a missing booking entry.
813 + error_log(
814 + sprintf(
815 + 'Timetics release_slot: no Booking_Entry found for booking #%d ( staff %s, meeting %s, slot %s %s ).',
816 + $this->id,
817 + $this->get_staff_id(),
818 + $this->get_appointment(),
819 + $start_date,
820 + $start_time
821 + )
721 822 );
722 823 }
723 - } elseif ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
724 - error_log(
725 - sprintf(
726 - 'Timetics release_slot: no Booking_Entry found for booking #%d ( staff %s, meeting %s ).',
727 - $this->id,
728 - $this->get_staff_id(),
729 - $this->get_appointment()
730 - )
731 - );
824 +
825 + return;
732 826 }
733 827
734 - update_post_meta( $this->id, $this->meta_prefix . 'slot_released', 1 );
828 + if ( 'one-to-one' === strtolower( $meeting->get_type() ) ) {
829 + $entry->delete();
830 +
831 + return;
832 + }
833 +
834 + $booked = max( 0, intval( $entry->get_booked() ) - 1 );
835 + $booked_seat = ! empty( $this->get_seat() ) ? $this->get_seat() : [];
836 + $existing_seat = ! empty( $entry->get_seats() ) ? $entry->get_seats() : [];
837 +
838 + $entry->update(
839 + [
840 + 'booked' => $booked,
841 + 'seats' => array_values( array_diff( $existing_seat, $booked_seat ) ),
842 + ]
843 + );
735 844 }
736 845
737 846 /**
738 847 * Delete booking