s[] = $rate_limit_id; } return true; } /** * Get order withdrawal rate limit identifiers for the current request. * * @param array $data Form data. * @return string[] */ private function get_rate_limit_ids( array $data ): array { $rate_limit_ids = array(); $ip_address = WC_Geolocation::get_ip_address(); $email = strtolower( trim( $data[ self::FIELD_EMAIL ] ) ); if ( '' !== $ip_address ) { $rate_limit_ids[] = self::RATE_LIMIT_IP_PREFIX . hash( 'sha256', $ip_address ); } if ( '' !== $email ) { $rate_limit_ids[] = self::RATE_LIMIT_EMAIL_PREFIX . hash( 'sha256', $email ); } return $rate_limit_ids; } /** * Get an order only when the submitted email and order number match. * * @param array $data Form data. */ private function get_matching_order( array $data ): ?WC_Order { $order_number = $this->normalize_order_number( $data[ self::FIELD_ORDER_NUMBER ] ); $email = $data[ self::FIELD_EMAIL ]; if ( '' === $order_number || '' === $email ) { return null; } if ( ctype_digit( $order_number ) ) { $order = wc_get_order( (int) $order_number ); if ( $order instanceof WC_Order && $this->order_matches_form_data( $order, $data ) ) { return $order; } } // Search by email first because the submitted order number may not be the internal order ID. $candidate_orders = wc_get_orders( array( 'billing_email' => $email, 'limit' => -1, 'orderby' => 'date', 'order' => 'DESC', 'return' => 'objects', ) ); if ( ! is_array( $candidate_orders ) ) { return null; } foreach ( $candidate_orders as $order ) { if ( ! $order instanceof WC_Order ) { continue; } if ( $this->normalize_order_number( (string) $order->get_order_number() ) !== $order_number ) { continue; } if ( $this->order_matches_form_data( $order, $data ) ) { return $order; } } return null; } /** * Whether a candidate order matches the submitted email and order number. * * @param WC_Order $order Candidate order. * @param array $data Form data. */ private function order_matches_form_data( WC_Order $order, array $data ): bool { return $this->normalize_order_number( (string) $order->get_order_number() ) === $this->normalize_order_number( $data[ self::FIELD_ORDER_NUMBER ] ) && $this->text_values_match( $order->get_billing_email( 'edit' ), $data[ self::FIELD_EMAIL ] ); } /** * Normalize a submitted order number for lookup and comparison. * * @param string $order_number Order number. */ private function normalize_order_number( string $order_number ): string { $order_number = trim( $order_number ); if ( 0 === strpos( $order_number, '#' ) ) { $order_number = trim( substr( $order_number, 1 ) ); } return $order_number; } /** * Compare submitted text values for identity while ignoring casing and surrounding spaces. * * @param string $stored_value Stored order value. * @param string $submitted_value Submitted form value. */ private function text_values_match( string $stored_value, string $submitted_value ): bool { return 0 === strcasecmp( trim( $stored_value ), trim( $submitted_value ) ); } /** * Add the withdrawal request note to a matched order. * * @param WC_Order $order Matched order. * @param array $data Form data. */ private function add_order_withdrawal_note( WC_Order $order, array $data ): void { $note = sprintf( /* translators: %s: withdrawal type label. */ __( 'Order withdrawal requested. Withdrawal type: %s.', 'woocommerce' ), $this->get_email_data_formatter()->get_withdrawal_type_label( $data[ self::FIELD_WITHDRAWAL_TYPE ] ?? '' ) ); try { if ( ! $order->add_order_note( $note, 0, false, array( 'note_group' => OrderNoteGroup::ORDER_UPDATE ) ) ) { $this->log_order_note_error( $order ); } } catch ( Throwable $e ) { $this->log_order_note_error( $order, $e ); } } /** * Add a withdrawal request notification to the merchant's WooCommerce inbox. * * @param WC_Order $matched_order Matched order. */ private function add_order_withdrawal_inbox_note( WC_Order $matched_order ): void { try { $content = sprintf( /* translators: %s: order number. */ __( 'A customer submitted an order withdrawal request for order #%s. Review the matched order to confirm the request details.', 'woocommerce' ), $matched_order->get_order_number() ); if ( $this->is_order_outside_withdrawal_window( $matched_order ) ) { $content .= ' ' . $this->get_withdrawal_window_warning_message(); } $note = new Note(); $note->set_title( sprintf( /* translators: %s: order number. */ __( 'Order withdrawal request for #%s', 'woocommerce' ), $matched_order->get_order_number() ) ); $note->set_content( $content ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::INBOX_NOTE_NAME_PREFIX . $matched_order->get_id() ); $note->set_source( 'woocommerce-admin' ); $order_url = $matched_order->get_edit_order_url(); if ( '' !== $order_url ) { $note->add_action( 'view-order', __( 'View order', 'woocommerce' ), $order_url ); } $note->save(); } catch ( Throwable $e ) { $this->log_inbox_note_error( $e, $matched_order->get_id() ); } } /** * Delete the withdrawal request inbox notification associated with an order. * * @param int|WC_Order $order Order ID or order object. */ public function delete_order_withdrawal_inbox_note_for_order( $order ): void { if ( $order instanceof WC_Order ) { $order_id = $order->get_id(); } elseif ( is_int( $order ) ) { if ( ! OrderUtil::is_order( $order ) ) { return; } $order_id = $order; } else { return; } if ( 0 >= $order_id ) { return; } try { Notes::delete_notes_with_name( self::INBOX_NOTE_NAME_PREFIX . $order_id ); } catch ( Throwable $e ) { $this->log_inbox_note_error( $e, $order_id ); } } /** * Whether the matched order is outside the valid withdrawal request window. * * @param WC_Order $order Matched order. */ private function is_order_outside_withdrawal_window( WC_Order $order ): bool { $date_created = $order->get_date_created( 'edit' ); if ( ! $date_created ) { return false; } return ( time() - self::WITHDRAWAL_WINDOW_IN_SECONDS ) > $date_created->getTimestamp(); } /** * Get the warning shown to merchants when a request is outside the valid window. */ private function get_withdrawal_window_warning_message(): string { return sprintf( /* translators: 1: number of days since the order was placed. 2: length of the withdrawal window in days. */ __( 'This order is older than %1$d days. Only orders within %2$d days of delivery are eligible for withdrawal.', 'woocommerce' ), self::WITHDRAWAL_WINDOW_IN_DAYS, self::WITHDRAWAL_WINDOW_IN_DAYS ); } /** * Whether the matched order already has a submitted withdrawal request. * * @param WC_Order $order Matched order. */ private function has_order_withdrawal_request( WC_Order $order ): bool { return self::ORDER_WITHDRAWAL_REQUESTED_VALUE === $order->get_meta( self::ORDER_WITHDRAWAL_REQUESTED_META_KEY, true, 'edit' ); } /** * Mark a matched order as having a submitted withdrawal request. * * @param WC_Order $order Matched order. */ private function mark_order_withdrawal_requested( WC_Order $order ): void { try { $order->update_meta_data( self::ORDER_WITHDRAWAL_REQUESTED_META_KEY, self::ORDER_WITHDRAWAL_REQUESTED_VALUE ); $order->save_meta_data(); } catch ( Throwable $e ) { $this->log_order_meta_error( $order, $e ); } } /** * Send customer and merchant order withdrawal emails. * * @param array $data Form data. * @param WC_Order|null $matched_order Matched order, if found. */ private function send_order_withdrawal_emails( array $data, ?WC_Order $matched_order ): bool { try { $submitted_at = time(); $customer_sent = $this->send_customer_order_withdrawal_email( $data, $submitted_at ); $merchant_sent = $this->send_merchant_order_withdrawal_email( $data, $matched_order, $submitted_at ); } catch ( Throwable $e ) { $this->log_email_error( $e ); return false; } if ( ! $customer_sent || ! $merchant_sent ) { $this->log_email_error( sprintf( 'Order withdrawal notification email failed. Customer email sent: %s. Merchant email sent: %s.', $customer_sent ? 'yes' : 'no', $merchant_sent ? 'yes' : 'no' ) ); return false; } return true; } /** * Send the customer order withdrawal acknowledgement email. * * @param array $data Form data. * @param int $submitted_at Unix timestamp for the submission. */ private function send_customer_order_withdrawal_email( array $data, int $submitted_at ): bool { $email = WC()->mailer()->get_emails()['WC_Email_Customer_Order_Withdrawal_Requested'] ?? include WC_ABSPATH . 'includes/emails/class-wc-email-customer-order-withdrawal-requested.php'; if ( ! $email instanceof WC_Email_Customer_Order_Withdrawal_Requested ) { return false; } if ( ! $email->is_enabled() ) { return true; } return $email->trigger( $data, $submitted_at ); } /** * Send the merchant order withdrawal notification email. * * @param array $data Form data. * @param WC_Order|null $matched_order Matched order, if found. * @param int $submitted_at Unix timestamp for the submission. */ private function send_merchant_order_withdrawal_email( array $data, ?WC_Order $matched_order, int $submitted_at ): bool { $email = WC()->mailer()->get_emails()['WC_Email_Order_Withdrawal_Requested'] ?? include WC_ABSPATH . 'includes/emails/class-wc-email-order-withdrawal-requested.php'; if ( ! $email instanceof WC_Email_Order_Withdrawal_Requested ) { return false; } if ( ! $email->is_enabled() ) { return true; } return $email->trigger( $data, $matched_order, $submitted_at, $matched_order instanceof WC_Order && $this->is_order_outside_withdrawal_window( $matched_order ), $matched_order instanceof WC_Order ? $this->get_withdrawal_window_warning_message() : '' ); } /** * Get the order withdrawal email data formatter. */ private function get_email_data_formatter(): OrderWithdrawalEmailDataFormatter { if ( null === $this->email_data_formatter ) { $this->email_data_formatter = new OrderWithdrawalEmailDataFormatter(); } return $this->email_data_formatter; } /** * Log an email failure. * * @param Throwable|string $error Email error. */ private function log_email_error( $error ): void { $message = $error instanceof Throwable ? $error->getMessage() : $error; wc_get_logger()->warning( sprintf( 'Order withdrawal email failed: %s', $message ), array( 'source' => self::LOGGER_SOURCE ) ); } /** * Log an inbox note failure without failing the submission. * * @param Throwable $e Inbox note error. * @param int $order_id Order ID. */ private function log_inbox_note_error( Throwable $e, int $order_id ): void { wc_get_logger()->warning( sprintf( 'Order withdrawal inbox note could not be processed for order %1$d. Error: %2$s', $order_id, $e->getMessage() ), array( 'source' => self::LOGGER_SOURCE ) ); } /** * Log an order note failure without failing the submission. * * @param WC_Order $order Matched order. * @param Throwable|null $e Order note error. */ private function log_order_note_error( WC_Order $order, ?Throwable $e = null ): void { $message = sprintf( 'Order withdrawal note could not be added to order %d.', $order->get_id() ); if ( $e instanceof Throwable ) { $message .= sprintf( ' Error: %s', $e->getMessage() ); } wc_get_logger()->warning( $message, array( 'source' => self::LOGGER_SOURCE ) ); } /** * Log an order meta failure without failing the submission. * * @param WC_Order $order Matched order. * @param Throwable $e Order meta error. */ private function log_order_meta_error( WC_Order $order, Throwable $e ): void { wc_get_logger()->warning( sprintf( 'Order withdrawal meta flag could not be added to order %1$d. Error: %2$s', $order->get_id(), $e->getMessage() ), array( 'source' => self::LOGGER_SOURCE ) ); } }