handle_cancellation( $order_id ); } /** * Schedule the review-request email when an order becomes complete. * * @internal * * @param int $order_id The completed order ID. */ public function handle_woocommerce_order_status_completed( int $order_id ): void { $order = wc_get_order( $order_id ); if ( ! $order instanceof WC_Order ) { return; } $email = $this->get_email(); if ( null === $email || ! $email->is_enabled() ) { $this->log_skip( $order_id, 'email is disabled' ); return; } if ( $order->get_meta( self::SCHEDULED_META_KEY ) ) { $this->log_skip( $order_id, 'already scheduled' ); return; } /** * Filter whether to schedule the review-request email for a given order. * * Return false to opt a specific order out of the automated email while * leaving the email enabled store-wide. * * @param bool $should_send Whether to schedule the email. Default true. * @param WC_Order $order The order being processed. * * @since 10.8.0 */ $should_send = (bool) apply_filters( 'woocommerce_should_send_review_request', true, $order ); if ( ! $should_send ) { $this->log_skip( $order_id, 'opt-out filter returned false' ); return; } // Don't queue an email whose link would land on the empty-state page: // every product on the order has reviews disabled (per-product or // site-wide via `woocommerce_enable_reviews`), or every reviewable // item already has a review tied to this order. if ( ! ItemEligibility::has_actionable_items( $order ) ) { $this->log_skip( $order_id, 'no reviewable items' ); return; } $when = time() + $email->get_delay_seconds(); as_schedule_single_action( $when, self::ACTION_HOOK, array( $order_id ) ); $order->update_meta_data( self::SCHEDULED_META_KEY, (string) $when ); $order->save(); } /** * Cancel any pending review-request action and clear the scheduled-at meta. * * Hooked directly into `woocommerce_trash_order` and * `woocommerce_before_delete_order` for the trash/delete lifecycle events, * and called from `handle_status_changed()` for every status transition * out of an eligible status (cancelled, refunded, processing, on-hold, * pending, failed, custom statuses…). * * @internal * * @param int $order_id The affected order ID. */ public function handle_cancellation( int $order_id ): void { // Always attempt to unschedule, even when the order or meta is missing, // so an out-of-sync meta value cannot leave a stray scheduled send. // `as_unschedule_action()` is a no-op when no matching action exists. as_unschedule_action( self::ACTION_HOOK, array( $order_id ) ); $order = wc_get_order( $order_id ); if ( $order instanceof WC_Order && $order->get_meta( self::SCHEDULED_META_KEY ) ) { $order->delete_meta_data( self::SCHEDULED_META_KEY ); $order->save(); } } /** * Retrieve the review-request email class instance from the mailer. */ private function get_email(): ?WC_Email_Customer_Review_Request { $mailer = WC()->mailer(); if ( ! $mailer ) { return null; } $emails = $mailer->get_emails(); $email = $emails['WC_Email_Customer_Review_Request'] ?? null; return $email instanceof WC_Email_Customer_Review_Request ? $email : null; } /** * Log a skipped scheduling attempt with the reason. * * @param int $order_id The order ID the attempt was for. * @param string $reason Human-readable reason the attempt was skipped. */ private function log_skip( int $order_id, string $reason ): void { wc_get_logger()->info( sprintf( /* translators: 1: order ID, 2: skip reason */ __( 'Skipped scheduling review-request email for order %1$d: %2$s.', 'woocommerce' ), $order_id, $reason ), array( 'source' => self::LOG_SOURCE ) ); } }