campaign_repository = new CampaignRepository(); $this->settings_repository = new SettingsRepository(); $this->log_repository = new MessageLogRepository(); } /** * Process a campaign batch. * * @param int $campaign_id Campaign ID. * @return array{processed:int,remaining:int} */ public function process_batch( int $campaign_id ): array { $campaign = $this->campaign_repository->get( $campaign_id ); if ( ! $campaign ) { return array( 'processed' => 0, 'remaining' => 0, ); } $settings = $this->settings_repository->get_all(); $limit = max( 1, min( 200, absint( $settings['batch_size'] ?? 20 ) ) ); $pending = $this->campaign_repository->get_pending_recipients( $campaign_id, $limit ); if ( empty( $pending ) ) { $this->campaign_repository->refresh_counters( $campaign_id ); return array( 'processed' => 0, 'remaining' => 0, ); } $this->campaign_repository->update_status( $campaign_id, 'processing' ); $client = ClientFactory::create( $settings ); $processed = 0; $channel = 'viber_sms' === $campaign->scheme ? 'viber' : (string) $campaign->scheme; foreach ( $pending as $recipient ) { if ( ! $this->campaign_repository->claim_recipient( (int) $recipient->id ) ) { continue; } $phone = (string) $recipient->phone; $fallback_message = isset( $campaign->fallback_message ) ? (string) $campaign->fallback_message : ''; if ( 'viber' === $channel ) { $result = $client->send_viber( (string) $campaign->message, array( $phone ), array( 'ttl' => absint( $settings['viber_ttl'] ?? 600 ) ) ); } else { $result = $client->send_sms( (string) $campaign->message, array( $phone ) ); } $status = $result->is_success() ? 'sent' : 'failed'; $error = $result->is_success() ? '' : $result->get_error(); $api_message_id = $this->extract_message_id( $result->get_data(), $phone ); $error_code = $result->is_success() ? '' : $this->derive_error_code( $result ); $this->campaign_repository->update_recipient( (int) $recipient->id, $status, $channel, $api_message_id, wp_json_encode( $result->get_raw() ), $error ); $this->log_repository->insert( array( 'campaign_id' => $campaign_id, 'order_id' => empty( $recipient->order_id ) ? null : (int) $recipient->order_id, 'send_type' => 'bulk', 'recipient_phone' => $phone, 'recipient_name' => (string) $recipient->name, 'recipient_role' => (string) $recipient->role, 'channel' => $channel, 'scheme' => (string) $campaign->scheme, 'message' => (string) $campaign->message, 'fallback_message' => $fallback_message, 'api_message_id' => $api_message_id, 'api_status' => $result->is_success() ? 'OK' : '', 'api_status_code' => $result->get_status_code(), 'error_code' => $error_code, 'api_response' => $this->encode_api_response( $result ), 'status' => $status, 'error_reason' => $error, 'viber_ttl' => 'viber' === $channel ? absint( $settings['viber_ttl'] ?? 600 ) : null, 'viber_sent_at' => 'viber' === $channel ? current_time( 'mysql' ) : null, 'status_updated_at' => current_time( 'mysql' ), ) ); PluginLogger::log( $result->is_success() ? 'info' : 'error', $result->is_success() ? 'campaign_recipient_sent' : 'campaign_recipient_failed', $result->is_success() ? __( 'Campaign recipient accepted by API.', 'inteltele' ) : $error, array( 'campaign_id' => $campaign_id, 'recipient_id' => (int) $recipient->id, 'phone' => $phone, 'scheme' => (string) $campaign->scheme, 'channel' => $channel, 'api_message_id' => $api_message_id, 'http_status' => $result->get_status_code(), 'error_code' => $error_code, ), 'CampaignProcessor' ); $processed++; } $this->campaign_repository->refresh_counters( $campaign_id ); $remaining = count( $this->campaign_repository->get_pending_recipients( $campaign_id, 1 ) ); return array( 'processed' => $processed, 'remaining' => $remaining, ); } /** * Process pending campaign batches from cron. * * @param int $limit Campaign limit. * @return array{campaigns:int,processed:int} */ public function process_pending_campaigns( int $limit = 10 ): array { $campaigns = $this->campaign_repository->get_processable( $limit ); $processed = 0; foreach ( $campaigns as $campaign ) { $result = $this->process_batch( (int) $campaign->id ); $processed += (int) $result['processed']; } PluginLogger::log( 'info', 'campaign_cron_processed', __( 'Pending bulk campaigns were processed by cron.', 'inteltele' ), array( 'campaigns' => count( $campaigns ), 'processed' => $processed, ), 'CampaignProcessor' ); return array( 'campaigns' => count( $campaigns ), 'processed' => $processed, ); } /** * Extract API message ID from normalized send data. * * @param array $data Data. * @param string $phone Phone. */ private function extract_message_id( array $data, string $phone ): string { $items = isset( $data['items'] ) && is_array( $data['items'] ) ? $data['items'] : array(); foreach ( $items as $item ) { if ( ! is_array( $item ) ) { continue; } $item_phone = isset( $item['number'] ) ? (string) $item['number'] : ''; if ( $item_phone === $phone || '' === $item_phone ) { return isset( $item['message_id'] ) ? (string) $item['message_id'] : ''; } } return ''; } /** * Encode API response for diagnostics. * * @param \IntelTele\Api\ApiResult $result API result. */ private function encode_api_response( \IntelTele\Api\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 ); } /** * Derive a stable error code for failed API requests. * * @param \IntelTele\Api\ApiResult $result API result. */ private function derive_error_code( \IntelTele\Api\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() ); } /** * 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 ); } }