Automattic\WooCommerce\Internal\OrderWithdrawal
OrderWithdrawalFormProcessor{} │ final │ WC 1.0
Processes order withdrawal form requests.
No Hooks.
Usage
$OrderWithdrawalFormProcessor = new OrderWithdrawalFormProcessor(); // use class methods
Methods
- public delete_order_withdrawal_inbox_note_for_order( $order )
- public static get_field_name( string $field_key )
- public process_current_request()
- private add_order_withdrawal_inbox_note( WC_Order $matched_order )
- private add_order_withdrawal_note( WC_Order $order, array $data )
- private add_validation_notices( array $errors )
- private apply_rate_limits( array $rate_limit_ids, int $delay = self::RATE_LIMIT_DELAY )
- private check_rate_limits( array $rate_limit_ids )
- private get_customer_name( array $data )
- private get_default_form_data()
- private get_email_details_html( array $data, int $submitted_at )
- private get_matching_order( array $data )
- private get_merchant_email_headers( array $data )
- private get_posted_action()
- private get_posted_form_data()
- private get_posted_text_value( string $field_name )
- private get_posted_textarea_value( string $field_name )
- private get_rate_limit_ids( array $data )
- private get_withdrawal_type_label( string $withdrawal_type )
- private get_withdrawal_window_warning_message()
- private has_order_withdrawal_request( WC_Order $order )
- private has_valid_nonce()
- private is_order_outside_withdrawal_window( WC_Order $order )
- private is_post_request()
- private log_email_error( $error )
- private log_inbox_note_error( Throwable $e, int $order_id )
- private log_order_meta_error( WC_Order $order, Throwable $e )
- private log_order_note_error( WC_Order $order, ?Throwable $e = null )
- private mark_order_withdrawal_requested( WC_Order $order )
- private normalize_order_number( string $order_number )
- private order_matches_form_data( WC_Order $order, array $data )
- private send_customer_order_withdrawal_email( array $data, int $submitted_at )
- private send_merchant_order_withdrawal_email( array $data, ?WC_Order $matched_order, int $submitted_at )
- private send_order_withdrawal_emails( array $data, ?WC_Order $matched_order )
- private submit_order_withdrawal( array $data )
- private text_values_match( string $stored_value, string $submitted_value )
- private validate_form_data( array $data )
- private wrap_email_message( string $heading, string $body )
OrderWithdrawalFormProcessor{} OrderWithdrawalFormProcessor{} code WC 11.1.2
final class OrderWithdrawalFormProcessor {
public const NONCE_ACTION = 'woocommerce_order_withdrawal';
public const NONCE_FIELD = 'woocommerce-order-withdrawal-nonce';
public const ACTION_FIELD = 'order_withdrawal_action';
public const ACTION_REVIEW = 'review';
public const ACTION_CONFIRM = 'confirm';
public const ACTION_EDIT = 'edit';
public const FIELD_PREFIX = 'order_withdrawal_';
public const FIELD_FIRST_NAME = 'first_name';
public const FIELD_LAST_NAME = 'last_name';
public const FIELD_EMAIL = 'email';
public const FIELD_EMAIL_CONFIRMATION = 'email_confirmation';
public const FIELD_ORDER_NUMBER = 'order_number';
public const FIELD_WITHDRAWAL_TYPE = 'withdrawal_type';
public const FIELD_ADDITIONAL_DETAILS = 'additional_details';
public const WITHDRAWAL_TYPE_FULL = 'full_order';
public const WITHDRAWAL_TYPE_SPECIFIC = 'specific_items_only';
private const LOGGER_SOURCE = 'order-withdrawal';
private const ORDER_WITHDRAWAL_REQUESTED_META_KEY = '_order_withdrawal_requested';
private const ORDER_WITHDRAWAL_REQUESTED_VALUE = 'yes';
private const WITHDRAWAL_WINDOW_IN_DAYS = 14;
private const WITHDRAWAL_WINDOW_IN_SECONDS = self::WITHDRAWAL_WINDOW_IN_DAYS * DAY_IN_SECONDS;
private const INBOX_NOTE_NAME_PREFIX = 'wc-order-withdrawal-requested-order-';
private const RATE_LIMIT_IP_PREFIX = 'order_withdrawal_ip_';
private const RATE_LIMIT_EMAIL_PREFIX = 'order_withdrawal_email_';
private const RATE_LIMIT_DELAY = MINUTE_IN_SECONDS / 2;
/**
* Process the current order withdrawal request.
*
* @since 11.1.0
*/
public function process_current_request(): OrderWithdrawalFormState {
$data = $this->get_default_form_data();
$errors = array();
$screen = 'form';
if ( ! $this->is_post_request() ) {
return new OrderWithdrawalFormState( $screen, $data, $errors );
}
if ( ! $this->has_valid_nonce() ) {
wc_add_notice( __( 'We could not verify your request. Please try again.', 'woocommerce' ), 'error' );
return new OrderWithdrawalFormState( $screen, $data, $errors );
}
$data = $this->get_posted_form_data();
$action = $this->get_posted_action();
if ( self::ACTION_EDIT === $action ) {
return new OrderWithdrawalFormState( $screen, $data, $errors );
}
$errors = $this->validate_form_data( $data );
if ( ! empty( $errors ) ) {
$this->add_validation_notices( $errors );
return new OrderWithdrawalFormState( $screen, $data, $errors );
}
if ( self::ACTION_CONFIRM === $action ) {
if ( ! $this->submit_order_withdrawal( $data ) ) {
return new OrderWithdrawalFormState( 'review', $data, $errors );
}
$screen = 'confirmation';
} else {
$screen = 'review';
}
return new OrderWithdrawalFormState( $screen, $data, $errors );
}
/**
* Get the posted name for a form field key.
*
* @param string $field_key Field key.
*
* @since 11.1.0
*/
public static function get_field_name( string $field_key ): string {
return self::FIELD_PREFIX . $field_key;
}
/**
* Get the default form data.
*
* @return array<string,string>
*/
private function get_default_form_data(): array {
return array(
self::FIELD_FIRST_NAME => '',
self::FIELD_LAST_NAME => '',
self::FIELD_EMAIL => '',
self::FIELD_EMAIL_CONFIRMATION => '',
self::FIELD_ORDER_NUMBER => '',
self::FIELD_WITHDRAWAL_TYPE => self::WITHDRAWAL_TYPE_FULL,
self::FIELD_ADDITIONAL_DETAILS => '',
);
}
/**
* Whether the current request is a form post.
*/
private function is_post_request(): bool {
$request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
return 'POST' === strtoupper( $request_method );
}
/**
* Get the submitted form action.
*/
private function get_posted_action(): string {
$action = $this->get_posted_text_value( self::ACTION_FIELD );
if ( in_array( $action, array( self::ACTION_REVIEW, self::ACTION_CONFIRM, self::ACTION_EDIT ), true ) ) {
return $action;
}
return self::ACTION_REVIEW;
}
/**
* Verify the order withdrawal form nonce.
*/
private function has_valid_nonce(): bool {
$nonce_value = $this->get_posted_text_value( self::NONCE_FIELD );
return '' !== $nonce_value && (bool) wp_verify_nonce( $nonce_value, self::NONCE_ACTION );
}
/**
* Get sanitized submitted form data.
*
* @return array<string,string>
*/
private function get_posted_form_data(): array {
return array(
self::FIELD_FIRST_NAME => $this->get_posted_text_value( self::get_field_name( self::FIELD_FIRST_NAME ) ),
self::FIELD_LAST_NAME => $this->get_posted_text_value( self::get_field_name( self::FIELD_LAST_NAME ) ),
self::FIELD_EMAIL => sanitize_email( $this->get_posted_text_value( self::get_field_name( self::FIELD_EMAIL ) ) ),
self::FIELD_EMAIL_CONFIRMATION => sanitize_email( $this->get_posted_text_value( self::get_field_name( self::FIELD_EMAIL_CONFIRMATION ) ) ),
self::FIELD_ORDER_NUMBER => $this->get_posted_text_value( self::get_field_name( self::FIELD_ORDER_NUMBER ) ),
self::FIELD_WITHDRAWAL_TYPE => $this->get_posted_text_value( self::get_field_name( self::FIELD_WITHDRAWAL_TYPE ) ),
self::FIELD_ADDITIONAL_DETAILS => $this->get_posted_textarea_value( self::get_field_name( self::FIELD_ADDITIONAL_DETAILS ) ),
);
}
/**
* Get a sanitized text value from the current POST request.
*
* @param string $field_name Field name.
*/
private function get_posted_text_value( string $field_name ): string {
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verification happens before submitted data is used.
if ( ! isset( $_POST[ $field_name ] ) || ! is_scalar( $_POST[ $field_name ] ) ) {
return '';
}
return sanitize_text_field( wp_unslash( (string) $_POST[ $field_name ] ) );
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
/**
* Get a sanitized textarea value from the current POST request.
*
* @param string $field_name Field name.
*/
private function get_posted_textarea_value( string $field_name ): string {
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verification happens before submitted data is used.
if ( ! isset( $_POST[ $field_name ] ) || ! is_scalar( $_POST[ $field_name ] ) ) {
return '';
}
return sanitize_textarea_field( wp_unslash( (string) $_POST[ $field_name ] ) );
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
/**
* Validate the form data.
*
* @param array<string,string> $data Form data.
* @return array<string,string>
*/
private function validate_form_data( array $data ): array {
$errors = array();
if ( '' === $data[ self::FIELD_FIRST_NAME ] ) {
$errors[ self::FIELD_FIRST_NAME ] = __( 'First name is a required field.', 'woocommerce' );
}
if ( '' === $data[ self::FIELD_LAST_NAME ] ) {
$errors[ self::FIELD_LAST_NAME ] = __( 'Last name is a required field.', 'woocommerce' );
}
if ( '' === $data[ self::FIELD_EMAIL ] || ! is_email( $data[ self::FIELD_EMAIL ] ) ) {
$errors[ self::FIELD_EMAIL ] = __( 'Enter a valid email address.', 'woocommerce' );
}
if ( '' === $data[ self::FIELD_EMAIL_CONFIRMATION ] ) {
$errors[ self::FIELD_EMAIL_CONFIRMATION ] = __( 'Confirm email address is a required field.', 'woocommerce' );
} elseif ( 0 !== strcasecmp( $data[ self::FIELD_EMAIL ], $data[ self::FIELD_EMAIL_CONFIRMATION ] ) ) {
$errors[ self::FIELD_EMAIL_CONFIRMATION ] = __( 'Email addresses do not match.', 'woocommerce' );
}
if ( '' === $data[ self::FIELD_ORDER_NUMBER ] ) {
$errors[ self::FIELD_ORDER_NUMBER ] = __( 'Order number is a required field.', 'woocommerce' );
}
if ( ! in_array( $data[ self::FIELD_WITHDRAWAL_TYPE ], array( self::WITHDRAWAL_TYPE_FULL, self::WITHDRAWAL_TYPE_SPECIFIC ), true ) ) {
$errors[ self::FIELD_WITHDRAWAL_TYPE ] = __( 'Choose what you want to withdraw.', 'woocommerce' );
}
if ( self::WITHDRAWAL_TYPE_SPECIFIC === $data[ self::FIELD_WITHDRAWAL_TYPE ] && '' === $data[ self::FIELD_ADDITIONAL_DETAILS ] ) {
$errors[ self::FIELD_ADDITIONAL_DETAILS ] = __( 'List the specific items you want to withdraw.', 'woocommerce' );
}
return $errors;
}
/**
* Add form validation notices.
*
* @param array<string,string> $errors Validation errors keyed by field.
*/
private function add_validation_notices( array $errors ): void {
foreach ( $errors as $field_key => $message ) {
wc_add_notice( $message, 'error', array( 'id' => self::get_field_name( $field_key ) ) );
}
}
/**
* Submit a validated order withdrawal request.
*
* @param array<string,string> $data Form data.
*/
private function submit_order_withdrawal( array $data ): bool {
$rate_limit_ids = $this->get_rate_limit_ids( $data );
if ( ! $this->check_rate_limits( $rate_limit_ids ) ) {
return false;
}
if ( ! $this->apply_rate_limits( $rate_limit_ids ) ) {
wc_add_notice( __( 'We could not submit your withdrawal request. Please try again or contact us if the problem continues.', 'woocommerce' ), 'error' );
return false;
}
$matched_order = $this->get_matching_order( $data );
if ( $matched_order && $this->has_order_withdrawal_request( $matched_order ) ) {
wc_add_notice(
__( 'A withdrawal request has already been submitted for this order. Please contact us if you need help or want to make changes.', 'woocommerce' ),
'error'
);
$this->apply_rate_limits( $rate_limit_ids, -1 );
return false;
}
if ( ! $this->send_order_withdrawal_emails( $data, $matched_order ) ) {
wc_add_notice( __( 'We could not submit your withdrawal request. Please try again or contact us if the problem continues.', 'woocommerce' ), 'error' );
$this->apply_rate_limits( $rate_limit_ids, -1 );
return false;
}
if ( $matched_order ) {
$this->mark_order_withdrawal_requested( $matched_order );
$this->add_order_withdrawal_note( $matched_order, $data );
$this->add_order_withdrawal_inbox_note( $matched_order );
}
return true;
}
/**
* Check order withdrawal submission rate limits.
*
* @param string[] $rate_limit_ids Rate limit IDs.
*/
private function check_rate_limits( array $rate_limit_ids ): bool {
foreach ( $rate_limit_ids as $rate_limit_id ) {
if ( WC_Rate_Limiter::retried_too_soon( $rate_limit_id ) ) {
wc_add_notice( __( 'Please wait before submitting another withdrawal request.', 'woocommerce' ), 'error' );
return false;
}
}
return true;
}
/**
* Set or clear the order withdrawal submission rate limits.
*
* @param string[] $rate_limit_ids Rate limit IDs.
* @param int $delay Delay in seconds for the rate limit. Use -1 to clear the rate limit.
* @return bool True if all rate limits were applied, false otherwise.
*/
private function apply_rate_limits( array $rate_limit_ids, int $delay = self::RATE_LIMIT_DELAY ): bool {
$applied_rate_limit_ids = array();
foreach ( $rate_limit_ids as $rate_limit_id ) {
if ( ! WC_Rate_Limiter::set_rate_limit( $rate_limit_id, $delay ) ) {
foreach ( $applied_rate_limit_ids as $applied_rate_limit_id ) {
WC_Rate_Limiter::set_rate_limit( $applied_rate_limit_id, -1 );
}
return false;
}
$applied_rate_limit_ids[] = $rate_limit_id;
}
return true;
}
/**
* Get order withdrawal rate limit identifiers for the current request.
*
* @param array<string,string> $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<string,string> $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<string,string> $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<string,string> $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_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<string,string> $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<string,string> $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 {
$subject = __( 'We received your withdrawal request', 'woocommerce' );
$heading = __( 'We received your withdrawal request', 'woocommerce' );
$body = '<p>' . esc_html__( 'We have received your request to withdraw from the order below.', 'woocommerce' ) . '</p>';
$body .= $this->get_email_details_html( $data, $submitted_at );
$body .= '<p>' . esc_html__( 'We will review your request and contact you about next steps, including any refund due.', 'woocommerce' ) . '</p>';
return wc_mail(
$data[ self::FIELD_EMAIL ],
$subject,
$this->wrap_email_message( $heading, $body )
);
}
/**
* Send the merchant order withdrawal notification email.
*
* @param array<string,string> $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 {
$recipient = sanitize_email( (string) get_option( 'admin_email' ) );
if ( '' === $recipient || ! is_email( $recipient ) ) {
return false;
}
$subject = sprintf(
/* translators: %s: order number. */
__( 'Order withdrawal request for order %s', 'woocommerce' ),
$data[ self::FIELD_ORDER_NUMBER ]
);
$heading = __( 'Order withdrawal request received', 'woocommerce' );
$body = '<p>' . esc_html__( 'A customer submitted an order withdrawal request.', 'woocommerce' ) . '</p>';
if ( $matched_order instanceof WC_Order ) {
$body .= '<p>' . esc_html__( 'WooCommerce matched this request to an order and added an order note.', 'woocommerce' ) . '</p>';
} else {
$body .= '<p>' . esc_html__( 'WooCommerce could not match this request to an order automatically, so no order note was added.', 'woocommerce' ) . '</p>';
}
$body .= $this->get_email_details_html( $data, $submitted_at );
if ( $matched_order instanceof WC_Order ) {
$order_url = $matched_order->get_edit_order_url();
if ( $this->is_order_outside_withdrawal_window( $matched_order ) ) {
$body .= '<p>' . esc_html( $this->get_withdrawal_window_warning_message() ) . '</p>';
}
$body .= sprintf(
'<p>%s</p>',
sprintf(
/* translators: %d: order ID. */
esc_html__( 'Matched order ID: %d', 'woocommerce' ),
$matched_order->get_id()
)
);
if ( '' !== $order_url ) {
$body .= sprintf(
'<p><a href="%1$s">%2$s</a></p>',
esc_url( $order_url ),
esc_html__( 'View matched order', 'woocommerce' )
);
}
}
return wc_mail(
$recipient,
$subject,
$this->wrap_email_message( $heading, $body ),
$this->get_merchant_email_headers( $data )
);
}
/**
* Get merchant email headers.
*
* @param array<string,string> $data Form data.
* @return string
*/
private function get_merchant_email_headers( array $data ): string {
$headers = array( 'Content-Type: text/html; charset=UTF-8' );
$name = $this->get_customer_name( $data );
$email = $data[ self::FIELD_EMAIL ];
if ( '' !== $name && is_email( $email ) ) {
$headers[] = sprintf( 'Reply-To: %1$s <%2$s>', $name, $email );
}
return implode( "\r\n", $headers );
}
/**
* Wrap an email body in the WooCommerce email template.
*
* @param string $heading Email heading.
* @param string $body Email body.
*/
private function wrap_email_message( string $heading, string $body ): string {
return WC()->mailer()->wrap_message( $heading, $body );
}
/**
* Get the email details list.
*
* @param array<string,string> $data Form data.
* @param int $submitted_at Unix timestamp for the submission.
*/
private function get_email_details_html( array $data, int $submitted_at ): string {
$date_format = (string) get_option( 'date_format' );
$time_format = (string) get_option( 'time_format' );
$additional_details = '' === $data[ self::FIELD_ADDITIONAL_DETAILS ] ? __( 'None provided', 'woocommerce' ) : $data[ self::FIELD_ADDITIONAL_DETAILS ];
$submitted_at_text = wp_date( trim( $date_format . ' ' . $time_format ), $submitted_at );
if ( false === $submitted_at_text ) {
$submitted_at_text = '';
}
$rows = array(
__( 'Submitted', 'woocommerce' ) => $submitted_at_text,
__( 'Name', 'woocommerce' ) => $this->get_customer_name( $data ),
__( 'Email address', 'woocommerce' ) => $data[ self::FIELD_EMAIL ],
__( 'Order number', 'woocommerce' ) => $data[ self::FIELD_ORDER_NUMBER ],
__( 'Withdrawing', 'woocommerce' ) => $this->get_withdrawal_type_label( $data[ self::FIELD_WITHDRAWAL_TYPE ] ),
__( 'Additional details', 'woocommerce' ) => $additional_details,
);
$html = '<ul>';
foreach ( $rows as $label => $value ) {
$html .= sprintf(
'<li><strong>%1$s:</strong> %2$s</li>',
esc_html( $label ),
nl2br( esc_html( $value ) )
);
}
$html .= '</ul>';
return $html;
}
/**
* Get the customer's full name for display.
*
* @param array<string,string> $data Form data.
*/
private function get_customer_name( array $data ): string {
return trim( $data[ self::FIELD_FIRST_NAME ] . ' ' . $data[ self::FIELD_LAST_NAME ] );
}
/**
* Get the label for a withdrawal type value.
*
* @param string $withdrawal_type Withdrawal type value.
*/
private function get_withdrawal_type_label( string $withdrawal_type ): string {
$options = array(
self::WITHDRAWAL_TYPE_FULL => __( 'The full order', 'woocommerce' ),
self::WITHDRAWAL_TYPE_SPECIFIC => __( 'Specific items only', 'woocommerce' ),
);
return $options[ $withdrawal_type ] ?? '';
}
/**
* 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 )
);
}
}