array( 'paid_google', 'google_ads' ), 'meta' => array( 'paid_facebook', 'meta' ), 'tiktok' => array( 'paid_tiktok', 'tiktok' ), ); const SECRETS = array( 'developer_token', 'client_secret', 'refresh_token', 'access_token' ); public function get_id() { return 'ad_connectors'; } public function get_icon() { return 'plug'; } public function get_label() { return __( 'Ad spend import', 'ai-eshop-optimizer' ); } public function get_group() { return 'setup'; } public function get_section() { return 'costs'; } public function get_priority() { return 16; } public function register_hooks() { if (is_admin()) { add_action('admin_enqueue_scripts', function () { if ((isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : '') === 'ai-eshop-dmm' && (isset($_GET['dmm_tab']) ? sanitize_text_field(wp_unslash($_GET['dmm_tab'])) : '') === 'ad_connectors') { $css = 'assets/css/aieo-ad-connectors-admin.css'; wp_enqueue_style('aieo-ad-connectors-admin', AIEO_URI . $css, array(), file_exists(AIEO_DIR . $css) ? (string) filemtime(AIEO_DIR . $css) : '1'); } }); } add_action( 'wp_ajax_aieo_adc_save', array( $this, 'ajax_save' ) ); add_action( 'wp_ajax_aieo_adc_test', array( $this, 'ajax_test' ) ); add_action( 'wp_ajax_aieo_adc_sync', array( $this, 'ajax_sync' ) ); add_action( self::CRON, array( $this, 'cron_sync_all' ) ); if ( ! wp_next_scheduled( self::CRON ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', self::CRON ); } } private static function key() { return hash( 'sha256', wp_salt( 'auth' ) . '|aieo_adconn', true ); } private static function enc( $s ) { $s = (string) $s; if ( '' === $s ) { return ''; } $iv = openssl_random_pseudo_bytes( 16 ); $c = openssl_encrypt( $s, 'aes-256-cbc', self::key(), OPENSSL_RAW_DATA, $iv ); return base64_encode( $iv . $c ); } private static function dec( $s ) { $raw = base64_decode( (string) $s, true ); if ( ! is_string( $raw ) || strlen( $raw ) < 17 ) { return ''; } $p = openssl_decrypt( substr( $raw, 16 ), 'aes-256-cbc', self::key(), OPENSSL_RAW_DATA, substr( $raw, 0, 16 ) ); return false === $p ? '' : $p; } private function get_creds( $platform ) { $all = get_option( self::OPTION, array() ); $row = isset( $all[ $platform ] ) && is_array( $all[ $platform ] ) ? $all[ $platform ] : array(); $out = array(); foreach ( $row as $k => $v ) { $out[ $k ] = in_array( $k, self::SECRETS, true ) ? self::dec( $v ) : $v; } return $out; } public static function credentials( $platform ) { $all = get_option( self::OPTION, array() ); $row = isset( $all[ $platform ] ) && is_array( $all[ $platform ] ) ? $all[ $platform ] : array(); $out = array(); foreach ( $row as $k => $v ) { $out[ $k ] = in_array( $k, self::SECRETS, true ) ? self::dec( $v ) : $v; } return $out; } private function configured( $platform ) { $c = $this->get_creds( $platform ); if ( 'google' === $platform ) { return ! empty( $c['customer_id'] ) && ! empty( $c['developer_token'] ) && ! empty( $c['client_id'] ) && ! empty( $c['refresh_token'] ); } if ( 'meta' === $platform ) { return ! empty( $c['ad_account_id'] ) && ! empty( $c['access_token'] ); } if ( 'tiktok' === $platform ) { return ! empty( $c['advertiser_id'] ) && ! empty( $c['access_token'] ); } return false; } private function guard() { check_ajax_referer( self::NONCE, 'nonce' ); if ( ! current_user_can( self::CAP ) ) { wp_send_json_error( array( 'msg' => 'forbidden' ), 403 ); } } public function ajax_save() { $this->guard(); $platform = isset( $_POST['platform'] ) ? sanitize_key( $_POST['platform'] ) : ''; if ( ! isset( self::PLATFORMS[ $platform ] ) ) { wp_send_json_error( array( 'msg' => 'bad platform' ) ); } $fields = array( 'customer_id', 'developer_token', 'client_id', 'client_secret', 'refresh_token', 'login_customer_id', 'ad_account_id', 'access_token', 'advertiser_id', 'auto' ); $all = get_option( self::OPTION, array() ); $cur = isset( $all[ $platform ] ) && is_array( $all[ $platform ] ) ? $all[ $platform ] : array(); foreach ( $fields as $f ) { if ( ! isset( $_POST[ $f ] ) ) { continue; } $v = sanitize_text_field( wp_unslash( $_POST[ $f ] ) ); if ( in_array( $f, self::SECRETS, true ) ) { if ( '' === $v ) { continue; } $cur[ $f ] = self::enc( $v ); } else { $cur[ $f ] = $v; } } $all[ $platform ] = $cur; update_option( self::OPTION, $all, false ); wp_send_json_success( array( 'configured' => $this->configured( $platform ) ) ); } public function ajax_test() { $this->guard(); $platform = isset( $_POST['platform'] ) ? sanitize_key( $_POST['platform'] ) : ''; $to = gmdate( 'Y-m-d', strtotime( '-1 day' ) ); $from = gmdate( 'Y-m-d', strtotime( '-7 day' ) ); $res = $this->fetch( $platform, $this->get_creds( $platform ), $from, $to ); if ( isset( $res['error'] ) ) { wp_send_json_error( array( 'msg' => $res['error'] ) ); } $total = 0.0; foreach ( $res['rows'] as $r ) { $total += $r['spend']; } wp_send_json_success( array( 'msg' => sprintf( __( 'OK — %1$d campaign-days, %2$s total spend (last 7 days). Not stored — use Sync to save.', 'ai-eshop-optimizer' ), count( $res['rows'] ), AIEO_DMM_Conversions_Registry::money( $total ) ) ) ); } public function ajax_sync() { $this->guard(); $platform = isset( $_POST['platform'] ) ? sanitize_key( $_POST['platform'] ) : ''; $from = isset( $_POST['from'] ) ? sanitize_text_field( wp_unslash( $_POST['from'] ) ) : ''; $to = isset( $_POST['to'] ) ? sanitize_text_field( wp_unslash( $_POST['to'] ) ) : ''; $d = '/^\d{4}-\d{2}-\d{2}$/'; if ( ! preg_match( $d, $to ) ) { $to = gmdate( 'Y-m-d', strtotime( '-1 day' ) ); } if ( ! preg_match( $d, $from ) ) { $from = gmdate( 'Y-m-d', strtotime( $to . ' -30 day' ) ); } $res = $this->sync( $platform, $from, $to ); if ( isset( $res['error'] ) ) { wp_send_json_error( array( 'msg' => $res['error'] ) ); } wp_send_json_success( array( 'msg' => sprintf( __( 'Synced %1$d campaign-day rows + %2$d channel-day rollups into the ledger.', 'ai-eshop-optimizer' ), $res['rows'], $res['days'] ) ) ); } public function cron_sync_all() { $to = gmdate( 'Y-m-d', strtotime( '-1 day' ) ); $from = gmdate( 'Y-m-d', strtotime( '-7 day' ) ); $status = array( 'ran_at' => gmdate( 'Y-m-d H:i:s' ), 'window' => $from . '..' . $to, 'platforms' => array() ); foreach ( array_keys( self::PLATFORMS ) as $platform ) { $c = $this->get_creds( $platform ); if ( empty( $c['auto'] ) ) { $status['platforms'][ $platform ] = array( 'state' => 'skipped', 'why' => 'auto sync off' ); continue; } if ( ! $this->configured( $platform ) ) { $status['platforms'][ $platform ] = array( 'state' => 'skipped', 'why' => 'credentials incomplete' ); continue; } $t0 = microtime( true ); $res = $this->sync( $platform, $from, $to ); $secs = round( microtime( true ) - $t0, 1 ); if ( isset( $res['error'] ) ) { $status['platforms'][ $platform ] = array( 'state' => 'error', 'why' => substr( (string) $res['error'], 0, 300 ), 'secs' => $secs ); aieo_debug_log( 'AIEO ad-connectors: ' . $platform . ' FAILED — ' . $res['error'] ); continue; } $rows = isset( $res['rows'] ) ? (int) $res['rows'] : 0; $status['platforms'][ $platform ] = array( 'state' => $rows > 0 ? 'ok' : 'warning', 'rows' => $rows, 'days' => isset( $res['days'] ) ? (int) $res['days'] : 0, 'why' => $rows > 0 ? '' : 'connected but returned no rows for the window — verify in the ad platform whether that is really zero spend', 'secs' => $secs, ); } update_option( self::STATUS_OPT, $status, false ); aieo_debug_log( 'AIEO ad-connectors: ' . wp_json_encode( $status ) ); return $status; } public static function last_sync_status() { $s = get_option( self::STATUS_OPT, array() ); $problems = array(); foreach ( (array) ( $s['platforms'] ?? array() ) as $p => $row ) { if ( in_array( ( $row['state'] ?? '' ), array( 'error', 'warning' ), true ) ) { $problems[] = $p . ': ' . ( $row['state'] ?? '' ) . ' — ' . ( $row['why'] ?? '' ); } } $s['problems'] = $problems; $s['never_ran'] = empty( $s['ran_at'] ); return $s; } private function fetch( $platform, $c, $from, $to ) { if ( ! $this->configured( $platform ) ) { return array( 'error' => __( 'Not configured — fill the credentials and Save first.', 'ai-eshop-optimizer' ) ); } if ( 'google' === $platform ) { return $this->google_fetch( $c, $from, $to ); } if ( 'meta' === $platform ) { return $this->meta_fetch( $c, $from, $to ); } if ( 'tiktok' === $platform ) { return $this->tiktok_fetch( $c, $from, $to ); } return array( 'error' => 'unknown platform' ); } public function sync( $platform, $from, $to ) { $res = $this->fetch( $platform, $this->get_creds( $platform ), $from, $to ); if ( isset( $res['error'] ) ) { return $res; } list( $chan, $src ) = self::PLATFORMS[ $platform ]; $n = 0; $by_day = array(); foreach ( $res['rows'] as $row ) { $date = preg_match( '/^\d{4}-\d{2}-\d{2}/', (string) $row['date'] ) ? substr( $row['date'], 0, 10 ) : $from; if ( '' !== (string) $row['campaign'] ) { AIEO_Attribution_Cost::upsert( 'campaign', $row['campaign'], $date, $date, $row['spend'], $src ); $n++; } $by_day[ $date ] = ( $by_day[ $date ] ?? 0 ) + (float) $row['spend']; } foreach ( $by_day as $date => $amt ) { AIEO_Attribution_Cost::upsert( 'channel', $chan, $date, $date, $amt, $src ); } return array( 'rows' => $n, 'days' => count( $by_day ) ); } private function google_fetch( $c, $from, $to ) { $tr = wp_remote_post( 'https://oauth2.googleapis.com/token', array( 'timeout' => 25, 'body' => array( 'client_id' => $c['client_id'], 'client_secret' => $c['client_secret'] ?? '', 'refresh_token' => $c['refresh_token'], 'grant_type' => 'refresh_token', ), ) ); if ( is_wp_error( $tr ) ) { return array( 'error' => 'token: ' . $tr->get_error_message() ); } $tok = json_decode( wp_remote_retrieve_body( $tr ), true ); if ( empty( $tok['access_token'] ) ) { return array( 'error' => 'token: ' . ( $tok['error_description'] ?? 'no access_token' ) ); } $cid = preg_replace( '/\D/', '', (string) $c['customer_id'] ); $headers = array( 'Authorization' => 'Bearer ' . $tok['access_token'], 'developer-token' => $c['developer_token'], 'Content-Type' => 'application/json', ); if ( ! empty( $c['login_customer_id'] ) ) { $headers['login-customer-id'] = preg_replace( '/\D/', '', (string) $c['login_customer_id'] ); } $q = "SELECT campaign.name, metrics.cost_micros, segments.date FROM campaign WHERE segments.date BETWEEN '{$from}' AND '{$to}'"; $versions = self::google_api_versions(); $cached = get_option( self::GOOGLE_VER_OPT ); if ( $cached && in_array( $cached, $versions, true ) ) { array_unshift( $versions, $cached ); $versions = array_values( array_unique( $versions ) ); } $last_err = ''; foreach ( $versions as $ver ) { $r = wp_remote_post( "https://googleads.googleapis.com/{$ver}/customers/{$cid}/googleAds:searchStream", array( 'timeout' => 45, 'headers' => $headers, 'body' => wp_json_encode( array( 'query' => $q ) ), ) ); if ( is_wp_error( $r ) ) { $last_err = $r->get_error_message(); continue; } $code = (int) wp_remote_retrieve_response_code( $r ); $raw = (string) wp_remote_retrieve_body( $r ); $body = json_decode( $raw, true ); if ( 200 !== $code ) { $msg = is_array( $body ) && isset( $body['error']['message'] ) ? (string) $body['error']['message'] : 'non-JSON response (likely a sunset API version)'; $last_err = sprintf( '%s HTTP %d: %s', $ver, $code, substr( $msg, 0, 200 ) ); continue; } if ( ! is_array( $body ) ) { $last_err = sprintf( '%s HTTP 200 but body was not JSON', $ver ); continue; } if ( isset( $body['error'] ) ) { $last_err = 'google: ' . ( $body['error']['message'] ?? 'api error' ); continue; } if ( $cached !== $ver ) { update_option( self::GOOGLE_VER_OPT, $ver, false ); } $out = array(); foreach ( $body as $chunk ) { foreach ( ( $chunk['results'] ?? array() ) as $row ) { $out[] = array( 'campaign' => $row['campaign']['name'] ?? '', 'date' => $row['segments']['date'] ?? $from, 'spend' => ( $row['metrics']['costMicros'] ?? 0 ) / 1000000, ); } } return array( 'rows' => $out, 'api_version' => $ver ); } return array( 'error' => 'google: no supported API version responded. Last: ' . $last_err ); } private static function google_api_versions() { return apply_filters( 'aieo_google_ads_api_versions', array( 'v22', 'v21', 'v20', 'v19' ) ); } private function meta_fetch( $c, $from, $to ) { $acct = 'act_' . ltrim( (string) $c['ad_account_id'], 'act_' ); $url = add_query_arg( array( 'level' => 'campaign', 'fields' => 'campaign_name,spend', 'time_increment' => 1, 'time_range' => wp_json_encode( array( 'since' => $from, 'until' => $to ) ), 'limit' => 500, 'access_token' => $c['access_token'], ), "https://graph.facebook.com/v20.0/{$acct}/insights" ); $out = array(); $pages = 0; while ( '' !== (string) $url && $pages < 1000 ) { $pages++; if ( function_exists( 'set_time_limit' ) ) { @set_time_limit( 60 ); } $r = wp_remote_get( $url, array( 'timeout' => 45 ) ); if ( is_wp_error( $r ) ) { return array( 'error' => $r->get_error_message() ); } $j = json_decode( wp_remote_retrieve_body( $r ), true ); if ( isset( $j['error'] ) ) { return array( 'error' => 'meta: ' . ( $j['error']['message'] ?? 'api error' ) ); } foreach ( ( $j['data'] ?? array() ) as $row ) { $out[] = array( 'campaign' => $row['campaign_name'] ?? '', 'date' => $row['date_start'] ?? $from, 'spend' => (float) ( $row['spend'] ?? 0 ), ); } $url = isset( $j['paging']['next'] ) ? (string) $j['paging']['next'] : ''; } return array( 'rows' => $out ); } private function tiktok_fetch( $c, $from, $to ) { $base = 'https://business-api.tiktok.com/open_api/v1.3/report/integrated/get/'; $out = array(); $page = 1; $pages_total = 1; do { if ( function_exists( 'set_time_limit' ) ) { @set_time_limit( 60 ); } $url = add_query_arg( array( 'advertiser_id' => $c['advertiser_id'], 'report_type' => 'BASIC', 'data_level' => 'AUCTION_CAMPAIGN', 'dimensions' => wp_json_encode( array( 'campaign_id', 'stat_time_day' ) ), 'metrics' => wp_json_encode( array( 'spend', 'campaign_name' ) ), 'start_date' => $from, 'end_date' => $to, 'page' => $page, 'page_size' => 1000, ), $base ); $r = wp_remote_get( $url, array( 'timeout' => 45, 'headers' => array( 'Access-Token' => $c['access_token'] ) ) ); if ( is_wp_error( $r ) ) { return array( 'error' => $r->get_error_message() ); } $j = json_decode( wp_remote_retrieve_body( $r ), true ); if ( (int) ( $j['code'] ?? -1 ) !== 0 ) { return array( 'error' => 'tiktok: ' . ( $j['message'] ?? 'api error' ) ); } foreach ( ( $j['data']['list'] ?? array() ) as $row ) { $dim = $row['dimensions'] ?? array(); $mt = $row['metrics'] ?? array(); $out[] = array( 'campaign' => $mt['campaign_name'] ?? ( $dim['campaign_id'] ?? '' ), 'date' => $dim['stat_time_day'] ?? $from, 'spend' => (float) ( $mt['spend'] ?? 0 ), ); } $pages_total = (int) ( $j['data']['page_info']['total_page'] ?? 1 ); $page++; } while ( $page <= $pages_total && $page <= 1000 ); return array( 'rows' => $out ); } public function render_subtab() { $nonce = wp_create_nonce( self::NONCE ); rooster_admin_band_open( array( 'cols' => 1 ) ); list( $open, $close ) = AIEO_DMM_UI::card( array( 'anchor' => 'ad-connectors-ad-spend-connectors', 'title' => __( 'How ad spend arrives', 'ai-eshop-optimizer' ), 'description' => __( 'Explains the nightly, read-only spend pull from Google Ads, Meta and TikTok, and is where you connect each ad account.', 'ai-eshop-optimizer' ), ) ); echo $open; echo \Rooster\Admin\Guidance::text( __( 'Connect Google Ads, Meta and TikTok once; Rooster One pulls campaign spend nightly into the ad-spend ledger, so the ROI Wizard shows true ROAS / ROI without you ever leaving Rooster One — one cockpit for spend AND your own reco / offer / channel attribution. Credentials are stored encrypted; the spend pull is read-only.', 'ai-eshop-optimizer' ) ); echo $close; $this->render_sync_status_card(); $this->render_wiring_card(); rooster_admin_band_close(); rooster_admin_band_open( array( 'cols' => 2 ) ); $this->panel( 'google', __( 'Google Ads', 'ai-eshop-optimizer' ), array( 'customer_id' => __( 'Customer ID (10-digit, e.g. 123-456-7890)', 'ai-eshop-optimizer' ), 'developer_token' => __( 'Developer token (secret)', 'ai-eshop-optimizer' ), 'client_id' => __( 'OAuth Client ID', 'ai-eshop-optimizer' ), 'client_secret' => __( 'OAuth Client secret (secret)', 'ai-eshop-optimizer' ), 'refresh_token' => __( 'OAuth Refresh token (secret)', 'ai-eshop-optimizer' ), 'login_customer_id' => __( 'Manager (MCC) ID — optional', 'ai-eshop-optimizer' ), ), __( 'Ask your agency for: the Customer ID, and either manager-account (MCC) access for our account OR a developer token + read-only OAuth credentials (Client ID/secret + a refresh token). The developer token is in the Google Ads API Center on the manager account; OAuth credentials in Google Cloud Console → APIs & Services → Credentials.', 'ai-eshop-optimizer' ), array( __( 'Google Ads → API Center', 'ai-eshop-optimizer' ) => 'https://ads.google.com/aw/apicenter', __( 'Cloud Console → Credentials', 'ai-eshop-optimizer' ) => 'https://console.cloud.google.com/apis/credentials', __( 'OAuth Playground (refresh token)', 'ai-eshop-optimizer' ) => 'https://developers.google.com/oauthplayground/', ), 'ad-connectors-configured', 1 ); $this->panel( 'meta', __( 'Meta (Facebook / Instagram)', 'ai-eshop-optimizer' ), array( 'ad_account_id' => __( 'Ad Account ID (act_… or the digits)', 'ai-eshop-optimizer' ), 'access_token' => __( 'Access token with ads_read (secret)', 'ai-eshop-optimizer' ), ), __( 'Ask your agency for: the Ad Account ID and a System-User access token with the ads_read permission — OR to add our Business/App as a partner with ads_read. Generated under business.facebook.com → System Users, or developers.facebook.com.', 'ai-eshop-optimizer' ), array( __( 'Business → System Users', 'ai-eshop-optimizer' ) => 'https://business.facebook.com/settings/system-users', __( 'Business → Ad Accounts', 'ai-eshop-optimizer' ) => 'https://business.facebook.com/settings/ad-accounts', __( 'Meta for Developers', 'ai-eshop-optimizer' ) => 'https://developers.facebook.com/apps/', ), 'ad-connectors-meta-facebook-instagram', 2 ); $this->panel( 'tiktok', __( 'TikTok', 'ai-eshop-optimizer' ), array( 'advertiser_id' => __( 'Advertiser ID', 'ai-eshop-optimizer' ), 'access_token' => __( 'Access token with reporting (secret)', 'ai-eshop-optimizer' ), ), __( 'Ask your agency for: the Advertiser ID and a long-lived access token with report read access — OR to add us as an authorised user. From the TikTok for Business / Marketing API app.', 'ai-eshop-optimizer' ), array( __( 'TikTok Marketing API portal', 'ai-eshop-optimizer' ) => 'https://business-api.tiktok.com/portal/', __( 'TikTok Ads Manager', 'ai-eshop-optimizer' ) => 'https://ads.tiktok.com/', ), 'ad-connectors-tiktok', 2 ); rooster_admin_band_close(); $this->print_js( $nonce ); } private function render_sync_status_card() { $s = self::last_sync_status(); $ran = empty( $s['never_ran'] ); list( $open, $close ) = AIEO_DMM_UI::card( array( 'anchor' => 'ad-connectors-last-sync', 'flush' => $ran && empty( $s['problems'] ), 'icon' => 'clock', 'title' => __( 'Last nightly sync', 'ai-eshop-optimizer' ), 'description' => __( 'The nightly import of ad spend from each connected platform: when it last ran and what each platform returned.', 'ai-eshop-optimizer' ), ) ); echo $open; if ( $ran ) { echo \Rooster\Admin\States::result_head( sprintf( __( 'Ran %s UTC', 'ai-eshop-optimizer' ), (string) ( $s['ran_at'] ?? '' ) ), array( 'facts' => array( sprintf( _x( 'covering %s', 'the days a sync covered', 'ai-eshop-optimizer' ), (string) ( $s['window'] ?? '' ) ), ), ) ); } if ( ! $ran ) { $never = \Rooster\Admin\States::state( 'not-run', array( 'text' => __( 'The nightly sync has not run yet on this site. Connect an account below and turn on auto-sync; the first run reports here.', 'ai-eshop-optimizer' ), 'attrs' => array( 'data-state' => 'never-ran' ), ) ); echo $never; echo $close; return; } if ( ! empty( $s['problems'] ) ) { $items = '
' . esc_html__( 'Campaign plans:', 'ai-eshop-optimizer' ) . ' ' . esc_html( implode( ' · ', $parts ) ) . '
'; } if ( null === $gov ) { echo rooster_admin_card_section_html( __( 'Paid boost governance', 'ai-eshop-optimizer' ), array( 'id' => 'aieo-section-ad-connectors-governance', 'description' => __( 'The paid boost actuator is not installed on this site, so there is nothing to govern.', 'ai-eshop-optimizer' ), ) ); echo $close; return; } echo rooster_admin_card_section_html( __( 'Paid boost governance', 'ai-eshop-optimizer' ), array( 'id' => 'aieo-section-ad-connectors-governance', 'description' => __( 'Four options decide whether the actuator can move a cent. They are shown here because nothing else in the product shows them — and deliberately not editable from this tab.', 'ai-eshop-optimizer' ), ) ); $caps = $this->caps_summary( $gov ); $gates = array( $this->wiring_row( __( 'Paid boosting', 'ai-eshop-optimizer' ), AIEO_Social_Boost::OPT_ENABLED, $gov['enabled'] ? __( 'on', 'ai-eshop-optimizer' ) : __( 'off', 'ai-eshop-optimizer' ), ! $gov['enabled'], $gov['enabled'] ? __( 'On. The gates below decide whether a boost is real or simulated.', 'ai-eshop-optimizer' ) : __( 'Off, as shipped. No boost can fire whatever the other three say.', 'ai-eshop-optimizer' ) ), $this->wiring_row( __( 'Sandbox', 'ai-eshop-optimizer' ), AIEO_Social_Boost::OPT_SANDBOX, $gov['sandbox'] ? __( 'on', 'ai-eshop-optimizer' ) : __( 'off — live', 'ai-eshop-optimizer' ), $gov['sandbox'], $gov['sandbox'] ? __( 'On, as shipped. A boost is validated, capped and journalled, but no request reaches the ad platform and no money moves.', 'ai-eshop-optimizer' ) : __( 'Off. An accepted boost executes against the live ad account.', 'ai-eshop-optimizer' ) ), $this->wiring_row( __( 'Kill switch', 'ai-eshop-optimizer' ), AIEO_Social_Boost::OPT_KILL, $gov['killed'] ? __( 'engaged', 'ai-eshop-optimizer' ) : __( 'not engaged', 'ai-eshop-optimizer' ), ! $gov['killed'], $gov['killed'] ? __( 'Engaged. Every boost is refused and the campaigns Rooster One created were paused.', 'ai-eshop-optimizer' ) : __( 'Not engaged. Held in reserve as the one-call stop for everything at once.', 'ai-eshop-optimizer' ) ), $this->wiring_row( __( 'Budget caps', 'ai-eshop-optimizer' ), AIEO_Social_Boost::OPT_CAPS, '' === $caps ? __( 'none set', 'ai-eshop-optimizer' ) : $caps, '' !== $caps, '' === $caps ? __( 'No per-campaign lifetime cap, so a live boost is refused outright rather than spending blind.', 'ai-eshop-optimizer' ) : __( 'A live boost is refused above these figures.', 'ai-eshop-optimizer' ) ), ); $this->wiring_table( $gates, __( 'Gate', 'ai-eshop-optimizer' ), __( 'What that means', 'ai-eshop-optimizer' ) ); echo $close; } private function wiring_row( $label, $sub, $state, $green, $desc ) { return array( 'label' => (string) $label, 'sub' => (string) $sub, 'state' => (string) $state, 'tone' => $green ? 'success' : 'neutral', 'desc' => (string) $desc, ); } private function wiring_table( $rows, $first_label, $last_label ) { AIEO_Table::render( array( 'page_id' => $this->get_id(), 'rows' => $rows, 'sortable' => false, 'columns' => array( array( 'key' => 'label', 'label' => $first_label, 'subline' => 'sub' ), array( 'key' => 'state', 'label' => __( 'State', 'ai-eshop-optimizer' ), 'type' => 'badge', 'nowrap' => true, 'format' => array( 'tone_key' => 'tone' ), ), array( 'key' => 'desc', 'label' => $last_label, 'width' => '520px' ), ), ) ); } private function boost_governance() { if ( ! class_exists( 'AIEO_Social_Boost' ) ) { return null; } $caps = get_option( AIEO_Social_Boost::OPT_CAPS, array() ); $caps = is_array( $caps ) ? $caps : array(); $enabled = AIEO_Social_Boost::enabled(); $sandbox = AIEO_Social_Boost::sandbox(); $killed = AIEO_Social_Boost::killed(); $lifetime = isset( $caps['lifetime'] ) ? (float) $caps['lifetime'] : 0.0; $account = ( $this->configured( 'meta' ) || $this->configured( 'google' ) ); return array( 'enabled' => $enabled, 'sandbox' => $sandbox, 'killed' => $killed, 'lifetime' => $lifetime, 'account' => $account, 'daily' => isset( $caps['daily'] ) && is_array( $caps['daily'] ) ? $caps['daily'] : array(), 'can_spend' => ( $enabled && ! $killed && $lifetime > 0 && $account && ! $sandbox ), ); } private function boost_block_reason( array $gov ) { if ( ! empty( $gov['killed'] ) ) { return __( 'the kill switch is engaged', 'ai-eshop-optimizer' ); } if ( empty( $gov['enabled'] ) ) { return __( 'paid boosting is switched off', 'ai-eshop-optimizer' ); } if ( $gov['lifetime'] <= 0 ) { return __( 'no per-campaign budget cap is set', 'ai-eshop-optimizer' ); } if ( empty( $gov['account'] ) ) { return __( 'no ad account is connected, so it has nothing to spend through', 'ai-eshop-optimizer' ); } return __( 'sandbox mode is on', 'ai-eshop-optimizer' ); } private function caps_summary( array $gov ) { $parts = array(); if ( $gov['lifetime'] > 0 ) { $parts[] = sprintf( __( 'lifetime %s', 'ai-eshop-optimizer' ), AIEO_DMM_Conversions_Registry::money( (float) $gov['lifetime'] ) ); } foreach ( (array) $gov['daily'] as $platform => $amount ) { if ( (float) $amount <= 0 ) { continue; } $parts[] = sprintf( __( '%1$s daily %2$s', 'ai-eshop-optimizer' ), mb_substr( (string) $platform, 0, 20 ), AIEO_DMM_Conversions_Registry::money( (float) $amount ) ); } return implode( ' · ', $parts ); } private function ads_plan_counts() { if ( ! class_exists( 'AIEO_Ads_Plan' ) ) { return null; } global $wpdb; $t = AIEO_Ads_Plan::table(); if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) !== $t ) { return array(); } if ( ! $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$t}` LIKE %s", 'status' ) ) ) { return array(); } $rows = $wpdb->get_results( "SELECT status, COUNT(*) AS n FROM `{$t}` GROUP BY status", ARRAY_A ); $out = array(); foreach ( (array) $rows as $r ) { $out[ (string) $r['status'] ] = (int) $r['n']; } return $out; } private function panel( $platform, $title, $fields, $guidance, $links = array(), $anchor = '', $col = 1 ) { $c = $this->get_creds( $platform ); $done = $this->configured( $platform ); $form_id = 'aieo-card-' . $anchor . '-form'; $footer = '' . esc_html__( 'Where to get these:', 'ai-eshop-optimizer' ) . ' '; $parts = array(); foreach ( $links as $label => $url ) { $parts[] = '' . esc_html( $label ) . ' ↗'; } echo implode( ' · ', $parts ); echo '
'; } echo '