get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- one light query, cached by the caller. $wpdb->prepare( 'SELECT o.id AS id, o.status AS status, d.meta_value AS date, s.meta_value AS slot, m.meta_value AS mode, l.meta_value AS location, v.meta_value AS variant FROM %i o INNER JOIN %i d ON d.order_id = o.id AND d.meta_key = %s LEFT JOIN %i s ON s.order_id = o.id AND s.meta_key = %s LEFT JOIN %i m ON m.order_id = o.id AND m.meta_key = %s LEFT JOIN %i l ON l.order_id = o.id AND l.meta_key = %s LEFT JOIN %i v ON v.order_id = o.id AND v.meta_key = %s WHERE o.type = %s AND d.meta_value BETWEEN %s AND %s', $wpdb->prefix . 'wc_orders', $wpdb->prefix . 'wc_orders_meta', DAYSLOT_META, $wpdb->prefix . 'wc_orders_meta', DAYSLOT_SLOT_META, $wpdb->prefix . 'wc_orders_meta', DAYSLOT_MODE_META, $wpdb->prefix . 'wc_orders_meta', DAYSLOT_LOCATION_META, $wpdb->prefix . 'wc_orders_meta', DAYSLOT_VARIANT_META, 'shop_order', $start, $end ), ARRAY_A ); } else { $result = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- one light query, cached by the caller. $wpdb->prepare( 'SELECT p.ID AS id, p.post_status AS status, d.meta_value AS date, s.meta_value AS slot, m.meta_value AS mode, l.meta_value AS location, v.meta_value AS variant FROM %i p INNER JOIN %i d ON d.post_id = p.ID AND d.meta_key = %s LEFT JOIN %i s ON s.post_id = p.ID AND s.meta_key = %s LEFT JOIN %i m ON m.post_id = p.ID AND m.meta_key = %s LEFT JOIN %i l ON l.post_id = p.ID AND l.meta_key = %s LEFT JOIN %i v ON v.post_id = p.ID AND v.meta_key = %s WHERE p.post_type = %s AND d.meta_value BETWEEN %s AND %s', $wpdb->posts, $wpdb->postmeta, DAYSLOT_META, $wpdb->postmeta, DAYSLOT_SLOT_META, $wpdb->postmeta, DAYSLOT_MODE_META, $wpdb->postmeta, DAYSLOT_LOCATION_META, $wpdb->postmeta, DAYSLOT_VARIANT_META, 'shop_order', $start, $end ), ARRAY_A ); } foreach ( (array) $result as $row ) { if ( ! isset( $wanted[ $row['status'] ] ) ) { continue; } $rows[] = array( 'kind' => 'order', 'id' => (int) $row['id'], 'date' => (string) $row['date'], 'slot' => (string) $row['slot'], 'mode' => 'pickup' === $row['mode'] ? 'pickup' : 'delivery', 'location' => (string) $row['location'], 'variant' => (string) $row['variant'], 'status' => substr( (string) $row['status'], 3 ), ); } } return apply_filters( 'dayslot_occupancy_rows', $rows, $start, $end ); } /** * Pool of a booking row. */ function dayslot_row_pool( $row ) { return dayslot_pool_key( $row ); } /** * Turn rows into counts: array( date => array( pool => array( 'total' => n, 'slots' => array( key => n ) ) ) ). */ function dayslot_aggregate_rows( $rows ) { $out = array(); foreach ( $rows as $row ) { if ( ! empty( $row['no_count'] ) ) { continue; } $date = $row['date']; $pool = dayslot_row_pool( $row ); if ( ! isset( $out[ $date ][ $pool ] ) ) { $out[ $date ][ $pool ] = array( 'total' => 0, 'slots' => array(), ); } if ( empty( $row['no_total'] ) ) { ++$out[ $date ][ $pool ]['total']; } if ( $row['slot'] ) { foreach ( dayslot_capacity_keys( $row['slot'], isset( $row['variant'] ) ? $row['variant'] : '' ) as $key ) { $out[ $date ][ $pool ]['slots'][ $key ] = ( isset( $out[ $date ][ $pool ]['slots'][ $key ] ) ? $out[ $date ][ $pool ]['slots'][ $key ] : 0 ) + 1; } } } return $out; } /** * Booking window kept in the cache. */ function dayslot_occupancy_window() { $today = dayslot_today(); return array( $today->modify( '-1 day' )->format( 'Y-m-d' ), $today->modify( '+' . ( (int) dayslot_get_settings()['max_days'] + 1 ) . ' days' )->format( 'Y-m-d' ), ); } /** * Cached counts for the whole booking window. */ function dayslot_occupancy_all() { global $dayslot_occ_memo; if ( null !== $dayslot_occ_memo ) { return $dayslot_occ_memo; } list( $start, $end ) = dayslot_occupancy_window(); $cached = get_transient( DAYSLOT_OCC_CACHE ); if ( is_array( $cached ) && isset( $cached['start'], $cached['end'], $cached['data'] ) && $cached['start'] === $start && $cached['end'] === $end ) { $dayslot_occ_memo = $cached['data']; return $dayslot_occ_memo; } $dayslot_occ_memo = dayslot_aggregate_rows( dayslot_booking_rows( $start, $end ) ); set_transient( DAYSLOT_OCC_CACHE, array( 'start' => $start, 'end' => $end, 'data' => $dayslot_occ_memo, ), 12 * HOUR_IN_SECONDS ); return $dayslot_occ_memo; } /** * Forget the cached counts (called whenever bookings change). */ function dayslot_flush_occupancy() { global $dayslot_occ_memo; $dayslot_occ_memo = null; delete_transient( DAYSLOT_OCC_CACHE ); do_action( 'dayslot_occupancy_flushed' ); } /** * Counts for one date and the context's pool: array( 'total' => n, 'slots' => array( key => n ) ). * When rescheduling, the order itself is not counted. */ function dayslot_occupancy( $date, $ctx = array() ) { $ctx = dayslot_ctx( $ctx ); list( $start, $end ) = dayslot_occupancy_window(); if ( $date >= $start && $date <= $end ) { $all = dayslot_occupancy_all(); } else { $all = dayslot_aggregate_rows( dayslot_booking_rows( $date, $date ) ); } $pool = dayslot_pool_key( $ctx ); $occ = isset( $all[ $date ][ $pool ] ) ? $all[ $date ][ $pool ] : array( 'total' => 0, 'slots' => array(), ); if ( $ctx['exclude_order'] ) { $order = wc_get_order( $ctx['exclude_order'] ); if ( $order && $order->get_meta( DAYSLOT_META ) === $date && in_array( $order->get_status(), (array) dayslot_get_settings()['statuses'], true ) ) { $c = dayslot_order_choice( $order ); if ( dayslot_pool_key( dayslot_ctx_for_choice( $c ) ) === $pool ) { $occ['total'] = max( 0, $occ['total'] - 1 ); foreach ( $c['slot'] ? dayslot_capacity_keys( $c['slot'], $c['variant'] ) : array() as $key ) { if ( isset( $occ['slots'][ $key ] ) ) { $occ['slots'][ $key ] = max( 0, $occ['slots'][ $key ] - 1 ); } } } } } return $occ; } // -------------------------------------------------------------------------- // Cache invalidation // -------------------------------------------------------------------------- foreach ( array( 'woocommerce_new_order', 'woocommerce_update_order', 'woocommerce_order_status_changed', 'woocommerce_delete_order', 'woocommerce_trash_order', 'woocommerce_untrash_order', 'dayslot_order_values_saved' ) as $dayslot_hook ) { add_action( $dayslot_hook, 'dayslot_flush_occupancy' ); } unset( $dayslot_hook ); add_action( 'update_option_dayslot_settings', 'dayslot_flush_occupancy' ); // Legacy (post-based) order storage. add_action( 'clean_post_cache', function ( $post_id, $post ) { if ( $post && 'shop_order' === $post->post_type ) { dayslot_flush_occupancy(); } }, 10, 2 );