*/ private $form_values = array( 'message' => '', 'fallback_message' => '', 'phone' => '', 'scheme' => 'sms', ); /** * Render page. */ public function render(): void { $this->prepare_page(); $notice = $this->handle_single_submit(); $bulk_notice = $this->handle_bulk_submit(); if ( null === $notice ) { $notice = $bulk_notice; } $tabs = array( 'single' => __( 'Single', 'inteltele' ), 'bulk' => __( 'Bulk', 'inteltele' ), ); $tab = $this->get_current_tab( 'single', array_keys( $tabs ) ); $this->open_page( __( 'Send message', 'inteltele' ) ); $this->render_tabs( $tabs, $tab, 'inteltele-send' ); $this->render_step_notice(); $this->render_notice( $notice ); if ( 'bulk' === $tab ) { $this->render_bulk_tab(); } else { $this->render_single_tab(); } $this->close_page(); } /** * Handle single message submission. * * @return array{type:string,messages:string[]}|null */ private function handle_single_submit(): ?array { if ( ! $this->is_post_request() ) { return null; } $action = isset( $_POST['inteltele_action'] ) ? sanitize_key( wp_unslash( $_POST['inteltele_action'] ) ) : ''; if ( 'send_single' !== $action ) { return null; } if ( ! current_user_can( 'inteltele_manage' ) ) { return array( 'type' => 'error', 'messages' => array( __( 'You do not have permission to send IntelTele messages.', 'inteltele' ) ), ); } if ( ! isset( $_POST['inteltele_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['inteltele_nonce'] ) ), 'inteltele_send_single' ) ) { return array( 'type' => 'error', 'messages' => array( __( 'Security check failed. Please reload the page and try again.', 'inteltele' ) ), ); } $message = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : ''; $fallback_message = isset( $_POST['fallback_message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['fallback_message'] ) ) : ''; $phone = isset( $_POST['phone'] ) ? $this->normalize_phone( sanitize_text_field( wp_unslash( $_POST['phone'] ) ) ) : ''; $scheme = isset( $_POST['scheme'] ) ? sanitize_key( wp_unslash( $_POST['scheme'] ) ) : 'sms'; if ( isset( $_POST['transliterate'] ) ) { $message = Transliterator::transliterate( $message ); $fallback_message = Transliterator::transliterate( $fallback_message ); } $this->form_values = array( 'message' => $message, 'fallback_message' => $fallback_message, 'phone' => $phone, 'scheme' => $scheme, ); $errors = $this->validate_single_form( $message, $fallback_message, $phone, $scheme ); if ( ! empty( $errors ) ) { return array( 'type' => 'error', 'messages' => $errors, ); } $sender = new MessageSender(); $result = $sender->send_manual( $message, $phone, $scheme, 'viber_sms' === $scheme ? $fallback_message : '' ); return array( 'type' => $result['success'] ? 'success' : 'error', 'messages' => $result['messages'], ); } /** * Validate single send form. * * @param string $message Message. * @param string $fallback_message SMS fallback message. * @param string $phone Phone. * @param string $scheme Scheme. * @return string[] */ private function validate_single_form( string $message, string $fallback_message, string $phone, string $scheme ): array { $errors = array(); if ( '' === trim( $message ) ) { $errors[] = __( 'Message text is required.', 'inteltele' ); } $limit = 'sms' === $scheme ? 256 : 1000; if ( $this->string_length( $message ) > $limit ) { $errors[] = sprintf( /* translators: %d: character limit. */ __( 'Message text must not exceed %d characters.', 'inteltele' ), $limit ); } if ( 'viber_sms' === $scheme ) { if ( '' === trim( $fallback_message ) ) { $errors[] = __( 'SMS fallback text is required for Viber + SMS.', 'inteltele' ); } if ( $this->string_length( $fallback_message ) > 256 ) { $errors[] = __( 'SMS fallback text must not exceed 256 characters.', 'inteltele' ); } } if ( strlen( $phone ) < 10 || strlen( $phone ) > 15 ) { $errors[] = __( 'Recipient phone must contain 10 to 15 digits.', 'inteltele' ); } if ( ! in_array( $scheme, array( 'sms', 'viber', 'viber_sms' ), true ) ) { $errors[] = __( 'Selected sending scheme is invalid.', 'inteltele' ); } return $errors; } /** * Get UTF-8 string length. * * @param string $value Value. */ private function string_length( string $value ): int { if ( function_exists( 'mb_strlen' ) ) { return mb_strlen( $value ); } return strlen( $value ); } /** * Render submit notice. * * @param array{type:string,messages:string[]}|null $notice Notice data. */ private function render_notice( ?array $notice ): void { if ( null === $notice ) { return; } $class = 'success' === $notice['type'] ? 'notice-success' : 'notice-error'; echo '
'; foreach ( $notice['messages'] as $message ) { echo '

' . esc_html( $message ) . '

'; } echo '
'; } /** * Handle bulk campaign submission. * * @return array{type:string,messages:string[]}|null */ private function handle_bulk_submit(): ?array { if ( ! $this->is_post_request() ) { return null; } $action = isset( $_POST['inteltele_action'] ) ? sanitize_key( wp_unslash( $_POST['inteltele_action'] ) ) : ''; if ( 'start_bulk_campaign' !== $action ) { return null; } if ( ! current_user_can( 'inteltele_manage' ) ) { return array( 'type' => 'error', 'messages' => array( __( 'You do not have permission to send IntelTele messages.', 'inteltele' ) ), ); } if ( ! isset( $_POST['inteltele_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['inteltele_nonce'] ) ), 'inteltele_start_bulk_campaign' ) ) { return array( 'type' => 'error', 'messages' => array( __( 'Security check failed. Please reload the page and try again.', 'inteltele' ) ), ); } $message = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : ''; $fallback_message = isset( $_POST['fallback_message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['fallback_message'] ) ) : ''; $scheme = isset( $_POST['scheme'] ) ? sanitize_key( wp_unslash( $_POST['scheme'] ) ) : 'sms'; $mode = isset( $_POST['recipient_mode'] ) ? sanitize_key( wp_unslash( $_POST['recipient_mode'] ) ) : 'role'; $role = isset( $_POST['role'] ) ? sanitize_key( wp_unslash( $_POST['role'] ) ) : ''; $user_ids = isset( $_POST['user_ids'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['user_ids'] ) ) : array(); $user_search = isset( $_POST['user_search'] ) ? sanitize_text_field( wp_unslash( $_POST['user_search'] ) ) : ''; $order_statuses = isset( $_POST['order_statuses'] ) ? array_map( 'sanitize_key', (array) wp_unslash( $_POST['order_statuses'] ) ) : array(); $order_recipient_keys = isset( $_POST['order_recipient_keys'] ) ? array_map( 'sanitize_text_field', (array) wp_unslash( $_POST['order_recipient_keys'] ) ) : array(); $date_from = isset( $_POST['order_date_from'] ) ? sanitize_text_field( wp_unslash( $_POST['order_date_from'] ) ) : ''; $date_to = isset( $_POST['order_date_to'] ) ? sanitize_text_field( wp_unslash( $_POST['order_date_to'] ) ) : ''; $errors = $this->validate_bulk_form( $message, $fallback_message, $scheme, $mode, $role, $user_ids, $order_statuses ); if ( ! empty( $errors ) ) { return array( 'type' => 'error', 'messages' => $errors, ); } $recipients = $this->get_bulk_recipients( $mode, $role, $user_ids, $user_search, $order_statuses, $date_from, $date_to, $order_recipient_keys ); if ( empty( $recipients ) ) { return array( 'type' => 'error', 'messages' => array( __( 'No recipients with phone numbers were found.', 'inteltele' ) ), ); } $repository = new CampaignRepository(); $campaign_id = $repository->create( get_current_user_id(), $scheme, $message, $recipients, 'viber_sms' === $scheme ? $fallback_message : '' ); if ( 0 === $campaign_id ) { return array( 'type' => 'error', 'messages' => array( __( 'Campaign could not be created. Please try again.', 'inteltele' ) ), ); } $processor = new CampaignProcessor(); $processor->process_batch( $campaign_id ); $campaign = $repository->get( $campaign_id ); return array( 'type' => 'success', 'messages' => array( __( 'Bulk campaign created.', 'inteltele' ), $campaign ? sprintf( /* translators: 1: processed count, 2: total count. */ __( 'Processed %1$d of %2$d recipients.', 'inteltele' ), (int) $campaign->processed_count, (int) $campaign->recipients_total ) : '', ), ); } /** * Validate bulk campaign form. * * @param string $message Message. * @param string $scheme Scheme. * @param string $mode Recipient mode. * @param string $role Role. * @param int[] $user_ids User IDs. * @return string[] */ private function validate_bulk_form( string $message, string $fallback_message, string $scheme, string $mode, string $role, array $user_ids, array $order_statuses ): array { $errors = array(); if ( '' === trim( $message ) ) { $errors[] = __( 'Message text is required.', 'inteltele' ); } $message_limit = 'sms' === $scheme ? 256 : 1000; if ( $this->string_length( $message ) > $message_limit ) { $errors[] = sprintf( /* translators: %d: character limit. */ __( 'Message text must not exceed %d characters.', 'inteltele' ), $message_limit ); } if ( 'viber_sms' === $scheme ) { if ( '' === trim( $fallback_message ) ) { $errors[] = __( 'SMS fallback text is required for Viber + SMS.', 'inteltele' ); } if ( $this->string_length( $fallback_message ) > 256 ) { $errors[] = __( 'SMS fallback text must not exceed 256 characters.', 'inteltele' ); } } if ( ! in_array( $scheme, array( 'sms', 'viber', 'viber_sms' ), true ) ) { $errors[] = __( 'Selected sending scheme is invalid.', 'inteltele' ); } if ( ! in_array( $mode, array( 'role', 'users', 'users_filtered', 'orders' ), true ) ) { $errors[] = __( 'Recipient selection mode is invalid.', 'inteltele' ); } if ( 'role' === $mode && ! array_key_exists( $role, wp_roles()->roles ) ) { $errors[] = __( 'Selected role is invalid.', 'inteltele' ); } if ( 'users' === $mode && empty( $user_ids ) ) { $errors[] = __( 'Select at least one user.', 'inteltele' ); } if ( 'orders' === $mode && empty( $order_statuses ) ) { $errors[] = __( 'Select at least one order status.', 'inteltele' ); } return $errors; } /** * Render single send form. */ private function render_single_tab(): void { echo '
'; wp_nonce_field( 'inteltele_send_single', 'inteltele_nonce' ); echo ''; echo ''; $this->render_message_row(); $this->render_scheme_row(); $this->render_fallback_message_row(); $this->render_phone_row( __( 'Recipient', 'inteltele' ) ); echo ''; echo '

'; echo '
'; } /** * Render bulk wizard placeholder. */ private function render_bulk_tab(): void { $step = isset( $this->query['step'] ) ? absint( $this->query['step'] ) : 1; $step = min( max( $step, 1 ), 4 ); echo '
'; echo '

' . esc_html( sprintf( /* translators: %d: wizard step number. */ __( 'Step %d', 'inteltele' ), $step ) ) . '

'; if ( 1 === $step ) { $this->render_bulk_step_one(); } elseif ( 2 === $step ) { $this->render_bulk_step_two(); } elseif ( 3 === $step ) { $this->render_bulk_step_three(); } else { $this->render_bulk_step_four(); } echo '
'; } /** * Render bulk step one. */ private function render_bulk_step_one(): void { echo '
'; $this->render_view_nonce(); echo ''; echo ''; echo ''; echo '
'; echo '

'; echo '

'; echo '

'; echo '
'; echo '

'; echo '
'; } /** * Render bulk step two. */ private function render_bulk_step_two(): void { $mode = isset( $this->query['recipient_mode'] ) ? sanitize_key( $this->query['recipient_mode'] ) : 'role'; if ( 'users' === $mode || 'users_filtered' === $mode ) { $this->render_user_picker(); return; } if ( 'orders' === $mode ) { $this->render_order_buyers_picker(); return; } echo '
'; $this->render_view_nonce(); echo ''; echo ''; echo ''; echo ''; echo ' '; echo ''; $this->render_wizard_buttons( 1 ); echo '
'; } /** * Render bulk step three. */ private function render_bulk_step_three(): void { $mode = isset( $this->query['recipient_mode'] ) ? sanitize_key( $this->query['recipient_mode'] ) : 'role'; $role = isset( $this->query['role'] ) ? sanitize_key( $this->query['role'] ) : ''; $user_ids = isset( $this->query['user_ids'] ) ? array_map( 'absint', (array) $this->query['user_ids'] ) : array(); $user_search = isset( $this->query['user_search'] ) ? sanitize_text_field( $this->query['user_search'] ) : ''; $order_statuses = isset( $this->query['order_statuses'] ) ? array_map( 'sanitize_key', (array) $this->query['order_statuses'] ) : array(); $order_recipient_keys = isset( $this->query['order_recipient_keys'] ) ? array_map( 'sanitize_text_field', (array) $this->query['order_recipient_keys'] ) : array(); $date_from = isset( $this->query['order_date_from'] ) ? sanitize_text_field( $this->query['order_date_from'] ) : ''; $date_to = isset( $this->query['order_date_to'] ) ? sanitize_text_field( $this->query['order_date_to'] ) : ''; $message = isset( $this->query['message'] ) ? sanitize_textarea_field( $this->query['message'] ) : ''; $fallback_message = isset( $this->query['fallback_message'] ) ? sanitize_textarea_field( $this->query['fallback_message'] ) : ''; $scheme = isset( $this->query['scheme'] ) ? sanitize_key( $this->query['scheme'] ) : 'sms'; $this->form_values['message'] = $message; $this->form_values['fallback_message'] = $fallback_message; $this->form_values['scheme'] = $scheme; echo '
'; $this->render_view_nonce(); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ( $user_ids as $user_id ) { echo ''; } foreach ( $order_statuses as $status ) { echo ''; } foreach ( $order_recipient_keys as $recipient_key ) { echo ''; } echo ''; echo ''; echo ''; $this->render_message_row(); $this->render_scheme_row(); $this->render_fallback_message_row(); echo ''; $this->render_wizard_buttons( 2 ); echo '
'; } /** * Render bulk step four. */ private function render_bulk_step_four(): void { $mode = isset( $this->query['recipient_mode'] ) ? sanitize_key( $this->query['recipient_mode'] ) : 'role'; $role = isset( $this->query['role'] ) ? sanitize_key( $this->query['role'] ) : ''; $user_ids = isset( $this->query['user_ids'] ) ? array_map( 'absint', (array) $this->query['user_ids'] ) : array(); $user_search = isset( $this->query['user_search'] ) ? sanitize_text_field( $this->query['user_search'] ) : ''; $order_statuses = isset( $this->query['order_statuses'] ) ? array_map( 'sanitize_key', (array) $this->query['order_statuses'] ) : array(); $order_recipient_keys = isset( $this->query['order_recipient_keys'] ) ? array_map( 'sanitize_text_field', (array) $this->query['order_recipient_keys'] ) : array(); $date_from = isset( $this->query['order_date_from'] ) ? sanitize_text_field( $this->query['order_date_from'] ) : ''; $date_to = isset( $this->query['order_date_to'] ) ? sanitize_text_field( $this->query['order_date_to'] ) : ''; $message = isset( $this->query['message'] ) ? sanitize_textarea_field( $this->query['message'] ) : ''; $fallback_message = isset( $this->query['fallback_message'] ) ? sanitize_textarea_field( $this->query['fallback_message'] ) : ''; $scheme = isset( $this->query['scheme'] ) ? sanitize_key( $this->query['scheme'] ) : 'sms'; $recipients = $this->get_bulk_recipients( $mode, $role, $user_ids, $user_search, $order_statuses, $date_from, $date_to, $order_recipient_keys ); $this->form_values['message'] = $message; $this->form_values['fallback_message'] = $fallback_message; $this->form_values['scheme'] = $scheme; echo '
'; wp_nonce_field( 'inteltele_start_bulk_campaign', 'inteltele_nonce' ); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ( $user_ids as $user_id ) { echo ''; } foreach ( $order_statuses as $status ) { echo ''; } foreach ( $order_recipient_keys as $recipient_key ) { echo ''; } echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo '

'; echo '' . esc_html__( 'Back', 'inteltele' ) . ' '; echo ''; echo '

'; echo '
'; } /** * Build a state-preserving back URL for bulk wizard steps. * * @param int $step Step number. * @param string $mode Recipient mode. * @param string $role Role. * @param int[] $user_ids User IDs. * @param string $user_search User search. * @param string[] $order_statuses Order statuses. * @param string[] $order_recipient_keys Selected order recipient keys. * @param string $date_from Order date from. * @param string $date_to Order date to. * @param string $message Message. * @param string $fallback_message SMS fallback message. * @param string $scheme Scheme. */ private function get_bulk_step_back_url( int $step, string $mode, string $role, array $user_ids, string $user_search, array $order_statuses, array $order_recipient_keys, string $date_from, string $date_to, string $message, string $fallback_message, string $scheme ): string { $args = array( 'page' => 'inteltele-send', 'tab' => 'bulk', 'step' => $step, 'recipient_mode' => $mode, 'role' => $role, 'user_search' => $user_search, 'user_ids' => $user_ids, 'order_statuses' => $order_statuses, 'order_recipient_keys' => $order_recipient_keys, 'order_date_from' => $date_from, 'order_date_to' => $date_to, 'message' => $message, 'fallback_message' => $fallback_message, 'scheme' => $scheme, ); return $this->get_page_url( $args ); } /** * Render users picker placeholder. */ private function render_user_picker(): void { $search = isset( $this->query['s'] ) ? sanitize_text_field( $this->query['s'] ) : ''; $users = $this->get_users_with_phones( $search ); echo '
'; $this->render_view_nonce(); echo ''; echo ''; echo ''; echo ''; echo ' '; echo ''; echo '
'; echo '
'; $this->render_view_nonce(); echo ''; echo ''; echo ''; echo ''; echo ''; echo '

'; echo esc_html( sprintf( /* translators: 1: visible users, 2: unique phones. */ __( 'Visible users: %1$d. Unique phones on this page: %2$d.', 'inteltele' ), count( $users ), count( $this->unique_phones_from_recipients( $users ) ) ) ); echo '

'; echo '

'; echo ' '; echo '

'; echo ''; echo ''; echo ''; if ( empty( $users ) ) { echo ''; } foreach ( $users as $index => $user_data ) { echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo ''; echo '
' . esc_html__( 'No.', 'inteltele' ) . '' . esc_html__( 'Phone', 'inteltele' ) . '' . esc_html__( 'Name', 'inteltele' ) . '' . esc_html__( 'Role', 'inteltele' ) . '
' . esc_html__( 'No users with phone numbers found.', 'inteltele' ) . '
' . esc_html( (string) ( $index + 1 ) ) . '' . esc_html( $user_data['phone'] ) . '' . esc_html( $user_data['name'] ) . '' . esc_html( $user_data['role'] ) . '
'; echo '

'; $this->render_wizard_buttons( 1 ); echo '
'; } /** * Render WooCommerce order buyers picker. */ private function render_order_buyers_picker(): void { $selected_statuses = isset( $this->query['order_statuses'] ) ? array_map( 'sanitize_key', (array) $this->query['order_statuses'] ) : array( 'wc-processing', 'wc-completed' ); $date_from = isset( $this->query['order_date_from'] ) ? sanitize_text_field( $this->query['order_date_from'] ) : ''; $date_to = isset( $this->query['order_date_to'] ) ? sanitize_text_field( $this->query['order_date_to'] ) : ''; $recipients = $this->get_recipients_by_orders( $selected_statuses, $date_from, $date_to ); echo '
'; $this->render_view_nonce(); echo ''; echo ''; echo ''; echo ''; echo ''; echo '
' . esc_html__( 'Order statuses', 'inteltele' ) . ''; foreach ( $this->get_order_statuses() as $status_key => $status_label ) { echo ''; } echo '
'; echo '

'; echo '

'; echo '

' . esc_html( sprintf( /* translators: %d: unique phone count. */ __( 'Preview: %d unique phones with valid billing phone numbers.', 'inteltele' ), count( $recipients ) ) ) . '

'; echo '

'; echo '
'; if ( ! isset( $this->query['orders_preview'] ) ) { return; } echo '
'; $this->render_view_nonce(); echo ''; echo ''; echo ''; echo ''; foreach ( $selected_statuses as $status ) { echo ''; } echo ''; echo ''; echo '

' . esc_html__( 'Found WooCommerce buyers', 'inteltele' ) . '

'; echo '

'; echo esc_html( sprintf( /* translators: %d: contact count. */ __( 'Select contacts to include in the campaign. Found contacts: %d.', 'inteltele' ), count( $recipients ) ) ); echo '

'; echo '

'; echo ' '; echo '

'; echo ''; echo ''; echo ''; if ( empty( $recipients ) ) { echo ''; } foreach ( $recipients as $index => $recipient ) { echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo ''; echo '
' . esc_html__( 'No.', 'inteltele' ) . '' . esc_html__( 'Order ID', 'inteltele' ) . '' . esc_html__( 'Phone', 'inteltele' ) . '' . esc_html__( 'Name', 'inteltele' ) . '
' . esc_html__( 'No WooCommerce buyers with phone numbers found.', 'inteltele' ) . '
' . esc_html( (string) ( $index + 1 ) ) . '' . esc_html( isset( $recipient['order_id'] ) ? (string) $recipient['order_id'] : '' ) . '' . esc_html( isset( $recipient['phone'] ) ? (string) $recipient['phone'] : '' ) . '' . esc_html( isset( $recipient['name'] ) ? (string) $recipient['name'] : '' ) . '
'; $this->render_wizard_buttons( 1 ); echo '
'; } /** * Render SMS fallback message row for Viber + SMS. */ private function render_fallback_message_row(): void { $style = 'viber_sms' === $this->form_values['scheme'] ? '' : 'display:none;'; echo ''; echo ''; echo ''; echo '

' . esc_html__( 'Used only when the selected scheme is Viber + SMS.', 'inteltele' ) . '

'; echo ''; } /** * Render message row. */ private function render_message_row(): void { echo ''; echo ''; echo ''; echo '

' . esc_html__( 'SMS limit is 256 characters. Viber limit is 1000 characters.', 'inteltele' ) . '

'; echo '

'; echo ''; } /** * Render scheme row. */ private function render_scheme_row(): void { echo ''; echo ''; echo ''; echo ''; } /** * Render phone row. * * @param string $label Field label. */ private function render_phone_row( string $label ): void { echo ''; echo ''; echo ''; echo ''; } /** * Render wizard navigation buttons. * * @param int $back_step Back step. */ private function render_wizard_buttons( int $back_step ): void { echo '

'; echo '' . esc_html__( 'Back', 'inteltele' ) . ' '; echo ''; echo '

'; } /** * Get users with phone numbers. * * @param string $search Search. * @return array> */ private function get_users_with_phones( string $search = '' ): array { $args = array( 'number' => 50, 'orderby' => 'display_name', 'order' => 'ASC', ); if ( '' !== $search ) { $args['search'] = '*' . $search . '*'; $args['search_columns'] = array( 'user_login', 'user_nicename', 'display_name', 'user_email' ); } $users = get_users( $args ); $rows = array(); foreach ( $users as $user ) { $phone = $this->get_user_phone( (int) $user->ID ); if ( '' === $phone ) { continue; } if ( '' !== $search && false === stripos( $phone, $search ) && false === stripos( $user->display_name, $search ) ) { continue; } $rows[] = array( 'user_id' => (int) $user->ID, 'phone' => $phone, 'name' => (string) $user->display_name, 'role' => $this->format_user_roles( (array) $user->roles ), ); } return $rows; } /** * Get recipients for selected bulk mode. * * @param string $mode Recipient mode. * @param string $role Role. * @param int[] $user_ids User IDs. * @param string $user_search User search. * @param string[] $order_statuses Order statuses. * @param string $date_from Date from. * @param string $date_to Date to. * @return array> */ private function get_bulk_recipients( string $mode, string $role, array $user_ids, string $user_search, array $order_statuses, string $date_from, string $date_to, array $order_recipient_keys = array() ): array { if ( 'users' === $mode ) { return $this->get_recipients_by_user_ids( $user_ids ); } if ( 'users_filtered' === $mode ) { return $this->get_recipients_by_filtered_users( $user_search ); } if ( 'orders' === $mode ) { $recipients = $this->get_recipients_by_orders( $order_statuses, $date_from, $date_to ); if ( empty( $order_recipient_keys ) ) { return array(); } $selected = array(); foreach ( $order_recipient_keys as $recipient_key ) { $normalized_key = $this->normalize_order_recipient_key( (string) $recipient_key ); if ( '' !== $normalized_key ) { $selected[ $normalized_key ] = true; } } return array_values( array_filter( $recipients, function ( array $recipient ) use ( $selected ): bool { return isset( $selected[ $this->get_order_recipient_key( $recipient ) ] ); } ) ); } return $this->get_recipients_by_role( $role ); } /** * Get recipients by filtered user list. * * @param string $search Search. * @return array> */ private function get_recipients_by_filtered_users( string $search = '' ): array { $args = array( 'number' => 5000, 'orderby' => 'ID', 'order' => 'ASC', ); if ( '' !== $search ) { $args['search'] = '*' . $search . '*'; $args['search_columns'] = array( 'user_login', 'user_nicename', 'display_name', 'user_email' ); } $user_ids = array_map( static function ( $user ): int { return (int) $user->ID; }, get_users( $args ) ); return $this->get_recipients_by_user_ids( $user_ids ); } /** * Get recipients by WooCommerce orders. * * @param string[] $statuses Order statuses. * @param string $date_from Date from. * @param string $date_to Date to. * @return array> */ private function get_recipients_by_orders( array $statuses, string $date_from = '', string $date_to = '' ): array { if ( ! function_exists( 'wc_get_orders' ) ) { return array(); } $statuses = array_values( array_filter( array_map( 'sanitize_key', $statuses ) ) ); if ( empty( $statuses ) || in_array( 'all', $statuses, true ) ) { $statuses = array_keys( $this->get_order_statuses() ); } $args = array( 'limit' => 5000, 'status' => array_map( static function ( string $status ): string { return 0 === strpos( $status, 'wc-' ) ? substr( $status, 3 ) : $status; }, $statuses ), 'orderby' => 'date', 'order' => 'DESC', 'return' => 'objects', ); if ( '' !== $date_from || '' !== $date_to ) { $args['date_created'] = ( '' === $date_from ? '1970-01-01' : $date_from ) . '...' . ( '' === $date_to ? gmdate( 'Y-m-d' ) : $date_to ); } $orders = wc_get_orders( $args ); $recipients = array(); $seen = array(); foreach ( $orders as $order ) { if ( ! is_a( $order, 'WC_Order' ) ) { continue; } $phone = $this->normalize_phone( (string) $order->get_billing_phone() ); if ( '' === $phone || isset( $seen[ $phone ] ) ) { continue; } $seen[ $phone ] = true; $name = trim( (string) $order->get_billing_first_name() . ' ' . (string) $order->get_billing_last_name() ); $recipients[] = array( 'user_id' => (int) $order->get_user_id(), 'order_id' => (int) $order->get_id(), 'phone' => $phone, 'name' => '' === $name ? (string) $order->get_billing_email() : $name, 'role' => 'woocommerce_buyer', ); } return $recipients; } /** * Get stable key for an order-based recipient row. * * @param array $recipient Recipient. */ private function get_order_recipient_key( array $recipient ): string { $phone = isset( $recipient['phone'] ) ? (string) $recipient['phone'] : ''; return $this->normalize_order_recipient_key( $phone ); } /** * Normalize an order recipient selection key. * * @param string $key Selection key. */ private function normalize_order_recipient_key( string $key ): string { if ( false !== strpos( $key, ':' ) ) { $parts = explode( ':', $key ); $key = (string) end( $parts ); } return (string) preg_replace( '/\D+/', '', $key ); } /** * Get recipients by role. * * @param string $role Role. * @return array> */ private function get_recipients_by_role( string $role ): array { if ( '' === $role || ! array_key_exists( $role, wp_roles()->roles ) ) { return array(); } $users = get_users( array( 'role' => $role, 'number' => 1000, 'orderby' => 'ID', 'order' => 'ASC', ) ); $recipients = array(); foreach ( $users as $user ) { $phone = $this->get_user_phone( (int) $user->ID ); if ( '' === $phone ) { continue; } $recipients[] = array( 'user_id' => (int) $user->ID, 'phone' => $phone, 'name' => (string) $user->display_name, 'role' => $role, ); } return $recipients; } /** * Get recipients by selected users. * * @param int[] $user_ids User IDs. * @return array> */ private function get_recipients_by_user_ids( array $user_ids ): array { $recipients = array(); foreach ( array_unique( array_filter( array_map( 'absint', $user_ids ) ) ) as $user_id ) { $user = get_userdata( $user_id ); if ( ! $user ) { continue; } $phone = $this->get_user_phone( $user_id ); if ( '' === $phone ) { continue; } $recipients[] = array( 'user_id' => $user_id, 'phone' => $phone, 'name' => (string) $user->display_name, 'role' => $this->format_user_roles( (array) $user->roles ), ); } return $recipients; } /** * Get user phone from common WooCommerce meta fields. * * @param int $user_id User ID. */ private function get_user_phone( int $user_id ): string { foreach ( array( 'billing_phone', 'phone', 'shipping_phone' ) as $meta_key ) { $phone = $this->normalize_phone( (string) get_user_meta( $user_id, $meta_key, true ) ); if ( '' !== $phone ) { return $phone; } } return ''; } /** * Normalize Ukrainian phone numbers to 380XXXXXXXXX where possible. * * @param string $phone Raw phone. */ private function normalize_phone( string $phone ): string { $digits = preg_replace( '/\D+/', '', $phone ); $digits = null === $digits ? '' : $digits; if ( 10 === strlen( $digits ) && '0' === substr( $digits, 0, 1 ) ) { $digits = '38' . $digits; } if ( 11 === strlen( $digits ) && '8' === substr( $digits, 0, 1 ) ) { $digits = '3' . $digits; } if ( strlen( $digits ) < 10 || strlen( $digits ) > 15 ) { return ''; } return $digits; } /** * Get unique phones from recipient-like rows. * * @param array> $recipients Recipients. * @return string[] */ private function unique_phones_from_recipients( array $recipients ): array { $phones = array(); foreach ( $recipients as $recipient ) { if ( empty( $recipient['phone'] ) ) { continue; } $phones[ (string) $recipient['phone'] ] = true; } return array_keys( $phones ); } /** * Get WooCommerce order statuses. * * @return array */ private function get_order_statuses(): array { if ( function_exists( 'wc_get_order_statuses' ) ) { return array_merge( array( 'all' => __( 'All', 'inteltele' ) ), wc_get_order_statuses() ); } return array( 'all' => __( 'All', 'inteltele' ), 'wc-pending' => __( 'Pending payment', 'inteltele' ), 'wc-processing' => __( 'Processing', 'inteltele' ), 'wc-completed' => __( 'Completed', 'inteltele' ), 'wc-cancelled' => __( 'Cancelled', 'inteltele' ), 'wc-refunded' => __( 'Refunded', 'inteltele' ), 'wc-failed' => __( 'Failed', 'inteltele' ), ); } /** * Format user roles. * * @param string[] $roles Roles. */ private function format_user_roles( array $roles ): string { $wp_roles = wp_roles()->roles; $labels = array(); foreach ( $roles as $role ) { $labels[] = isset( $wp_roles[ $role ] ) ? translate_user_role( $wp_roles[ $role ]['name'] ) : $role; } return implode( ', ', $labels ); } /** * Format scheme. * * @param string $scheme Scheme. */ private function format_scheme( string $scheme ): string { $map = array( 'sms' => __( 'SMS', 'inteltele' ), 'viber' => __( 'Viber', 'inteltele' ), 'viber_sms' => __( 'Viber + SMS', 'inteltele' ), ); return $map[ $scheme ] ?? $scheme; } }