settings_repository = new SettingsRepository(); $this->log_repository = new MessageLogRepository(); } /** * Send a manual message. * * @param string $message Message text. * @param string $phone Recipient phone. * @param string $scheme Scheme. * @param string $fallback_message SMS fallback message. * @return array{success:bool,messages:string[]} */ public function send_manual( string $message, string $phone, string $scheme, string $fallback_message = '' ): array { return $this->send( $message, $phone, $scheme, array( 'send_type' => 'manual', 'fallback_message' => $fallback_message, ) ); } /** * Send an order status message. * * @param string $message Message text. * @param string $phone Recipient phone. * @param string $scheme Scheme. * @param array $context Context. * @return array{success:bool,messages:string[]} */ public function send_order_status( string $message, string $phone, string $scheme, array $context ): array { $context['send_type'] = 'order_status'; return $this->send( $message, $phone, $scheme, $context ); } /** * Send message. * * @param string $message Message text. * @param string $phone Recipient phone. * @param string $scheme Scheme. * @param array $context Context. * @return array{success:bool,messages:string[]} */ private function send( string $message, string $phone, string $scheme, array $context ): array { $settings = $this->settings_repository->get_all(); $client = ClientFactory::create( $settings ); $channel = 'viber_sms' === $scheme ? 'viber' : $scheme; $options = array(); if ( 'viber' === $channel ) { $options['ttl'] = absint( $settings['viber_ttl'] ?? 600 ); $result = $client->send_viber( $message, array( $phone ), $options ); } else { $result = $client->send_sms( $message, array( $phone ), $options ); } $this->store_result( $result, $message, $phone, $scheme, $channel, $options, $context ); if ( ! $result->is_success() ) { PluginLogger::log( 'error', 'message_send_failed', $result->get_error(), array( 'phone' => $phone, 'scheme' => $scheme, 'channel' => $channel, 'http_status' => $result->get_status_code(), 'error_code' => $this->derive_error_code( $result ), 'api_response' => $this->encode_api_response( $result ), 'send_type' => isset( $context['send_type'] ) ? (string) $context['send_type'] : 'manual', ), 'MessageSender' ); return array( 'success' => false, 'messages' => array( __( 'Message was not sent. Please try again.', 'inteltele' ), $result->get_error(), ), ); } $item_errors = $this->get_item_errors( $result ); if ( ! empty( $item_errors ) ) { return array( 'success' => false, 'messages' => array_merge( array( __( 'Message was not sent. Please try again.', 'inteltele' ) ), $item_errors ), ); } return array( 'success' => true, 'messages' => array( __( 'Message sent successfully.', 'inteltele' ) ), ); } /** * Store API send result. * * @param ApiResult $result API result. * @param string $message Message. * @param string $phone Phone. * @param string $scheme Scheme. * @param string $channel Channel. * @param array $options Options. * @param array $context Context. */ private function store_result( ApiResult $result, string $message, string $phone, string $scheme, string $channel, array $options, array $context ): void { $base = array( 'send_type' => isset( $context['send_type'] ) ? (string) $context['send_type'] : 'manual', 'order_id' => isset( $context['order_id'] ) ? absint( $context['order_id'] ) : null, 'recipient_name' => isset( $context['recipient_name'] ) ? (string) $context['recipient_name'] : '', 'recipient_role' => isset( $context['recipient_role'] ) ? (string) $context['recipient_role'] : '', 'fallback_message' => isset( $context['fallback_message'] ) ? (string) $context['fallback_message'] : '', 'duplicate_key' => isset( $context['duplicate_key'] ) ? (string) $context['duplicate_key'] : '', ); if ( ! $result->is_success() ) { $this->log_repository->insert( array_merge( $base, array( 'recipient_phone' => $phone, 'channel' => $channel, 'scheme' => $scheme, 'message' => $message, 'api_status_code' => $result->get_status_code(), 'error_code' => $this->derive_error_code( $result ), 'api_response' => $this->encode_api_response( $result ), 'status' => 'failed', 'error_reason' => $result->get_error(), 'viber_ttl' => isset( $options['ttl'] ) ? absint( $options['ttl'] ) : null, 'viber_sent_at' => 'viber' === $channel ? current_time( 'mysql' ) : null, ) ) ); return; } $data = $result->get_data(); $items = isset( $data['items'] ) && is_array( $data['items'] ) ? $data['items'] : array(); if ( empty( $items ) ) { $this->log_repository->insert( array_merge( $base, array( 'recipient_phone' => $phone, 'channel' => $channel, 'scheme' => $scheme, 'message' => $message, 'api_status_code' => $result->get_status_code(), 'api_response' => $this->encode_api_response( $result ), 'status' => 'sent', 'viber_ttl' => isset( $options['ttl'] ) ? absint( $options['ttl'] ) : null, 'viber_sent_at' => 'viber' === $channel ? current_time( 'mysql' ) : null, ) ) ); return; } foreach ( $items as $item ) { $item_phone = isset( $item['number'] ) ? (string) $item['number'] : $phone; $api_status = isset( $item['status'] ) ? (string) $item['status'] : ''; $is_sent = 'OK' === strtoupper( $api_status ); $error_code = $is_sent ? '' : $this->derive_item_error_code( $item, $api_status, $result ); $this->log_repository->insert( array_merge( $base, array( 'recipient_phone' => $item_phone, 'channel' => $channel, 'scheme' => $scheme, 'message' => $message, 'api_message_id' => isset( $item['message_id'] ) ? (string) $item['message_id'] : '', 'api_status' => $api_status, 'api_status_code' => $result->get_status_code(), 'error_code' => $error_code, 'api_response' => $this->encode_api_response( $result ), 'status' => $is_sent ? 'sent' : 'failed', 'error_reason' => $is_sent ? null : $api_status, 'viber_ttl' => isset( $options['ttl'] ) ? absint( $options['ttl'] ) : null, 'viber_sent_at' => 'viber' === $channel ? current_time( 'mysql' ) : null, 'status_updated_at' => current_time( 'mysql' ), ) ) ); PluginLogger::log( $is_sent ? 'info' : 'error', $is_sent ? 'message_sent' : 'message_send_failed', $is_sent ? __( 'Message accepted by API.', 'inteltele' ) : __( 'Message item failed in API response.', 'inteltele' ), array( 'phone' => $item_phone, 'scheme' => $scheme, 'channel' => $channel, 'api_message_id' => isset( $item['message_id'] ) ? (string) $item['message_id'] : '', 'api_status' => $api_status, 'http_status' => $result->get_status_code(), 'error_code' => $error_code, 'send_type' => isset( $context['send_type'] ) ? (string) $context['send_type'] : 'manual', ), 'MessageSender' ); } } /** * Encode API response for diagnostics. * * @param ApiResult $result API result. */ private function encode_api_response( ApiResult $result ): string { $raw = $result->get_raw(); if ( is_wp_error( $raw ) ) { $raw = array( 'errors' => $raw->errors, 'data' => $raw->error_data, ); } return (string) wp_json_encode( array( 'http_status' => $result->get_status_code(), 'data' => $result->get_data(), 'error' => $result->get_error(), 'raw' => $raw, ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); } /** * Get item-level API errors from a successful HTTP response. * * @param ApiResult $result API result. * @return string[] */ private function get_item_errors( ApiResult $result ): array { $data = $result->get_data(); $items = isset( $data['items'] ) && is_array( $data['items'] ) ? $data['items'] : array(); $errors = array(); foreach ( $items as $item ) { if ( ! is_array( $item ) ) { continue; } $status = isset( $item['status'] ) ? (string) $item['status'] : ''; if ( 'OK' === strtoupper( $status ) ) { continue; } $errors[] = '' === $status ? __( 'API did not return an item status.', 'inteltele' ) : HttpClient::translate_api_status( $status ); } return array_values( array_unique( $errors ) ); } /** * Derive a stable error code for failed API requests. * * @param ApiResult $result API result. */ private function derive_error_code( ApiResult $result ): string { $raw = $result->get_raw(); if ( is_wp_error( $raw ) ) { $codes = $raw->get_error_codes(); return isset( $codes[0] ) ? $this->normalize_error_code( (string) $codes[0] ) : 'wp_error'; } if ( $result->get_status_code() > 0 && ! $result->is_success() ) { return 'http_' . $result->get_status_code(); } return $this->normalize_error_code( $result->get_error() ); } /** * Derive item-level error code. * * @param array $item API response item. * @param string $api_status API status. * @param ApiResult $result API result. */ private function derive_item_error_code( array $item, string $api_status, ApiResult $result ): string { foreach ( array( 'error_code', 'code', 'status' ) as $key ) { if ( isset( $item[ $key ] ) && '' !== (string) $item[ $key ] ) { return $this->normalize_error_code( (string) $item[ $key ] ); } } if ( '' !== $api_status ) { return $this->normalize_error_code( $api_status ); } return $this->derive_error_code( $result ); } /** * Normalize arbitrary API/WP error text to a searchable code. * * @param string $value Error text. */ private function normalize_error_code( string $value ): string { $value = preg_replace( '/^error:\s*/i', '', $value ); $value = strtolower( trim( (string) $value ) ); $value = preg_replace( '/[^a-z0-9_:-]+/', '_', $value ); $value = trim( (string) $value, '_' ); return '' === $value ? 'unknown_error' : substr( $value, 0, 191 ); } }