get_results( $wpdb->prepare( "SELECT p.ID, p.post_title FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id INNER JOIN {$wpdb->postmeta} ps ON p.ID = ps.post_id AND ps.meta_key = '_easy_invoice_quote_status' WHERE p.post_type = %s AND p.post_status IN ('publish', 'draft') AND pm.meta_key = '_easy_invoice_quote_expiry_date' AND pm.meta_value <> '' AND pm.meta_value < %s AND ps.meta_value IN ('draft', 'available', 'sent')", \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE, $current_date ) ); if ( empty( $expired_quotes ) ) { return; } $updated_count = 0; foreach ( $expired_quotes as $quote ) { // Update the quote status to expired update_post_meta( $quote->ID, '_easy_invoice_quote_status', 'expired' ); // Fire integration hook — addons (Webhooks, etc.) subscribe to this. do_action( 'easy_invoice_quote_expired', $quote->ID ); $updated_count++; } } /** * Manually trigger quote expiration check * * @since 2.0.0 */ public static function manual_check_expired_quotes() { self::check_expired_quotes(); } /** * Clear the scheduled event on plugin deactivation * * @since 2.0.0 */ public static function clear_schedule() { wp_clear_scheduled_hook( 'easy_invoice_quote_expiration_check' ); } }