get_charset_collate(); $results = $wpdb->prefix . 'adeditors_results'; $tasks = $wpdb->prefix . 'adeditors_tasks'; dbDelta("CREATE TABLE $results ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, user_id bigint(20) unsigned NOT NULL DEFAULT 0, result_date date NOT NULL, platform varchar(50) NOT NULL DEFAULT '', campaign varchar(191) NOT NULL DEFAULT '', campaign_id bigint(20) unsigned NOT NULL DEFAULT 0, adset varchar(191) NOT NULL DEFAULT '', adset_id bigint(20) unsigned NOT NULL DEFAULT 0, ad_name varchar(191) NOT NULL DEFAULT '', ad_id bigint(20) unsigned NOT NULL DEFAULT 0, product_ids longtext NULL, spend decimal(18,4) NOT NULL DEFAULT 0, impressions bigint(20) unsigned NOT NULL DEFAULT 0, reach bigint(20) unsigned NOT NULL DEFAULT 0, clicks bigint(20) unsigned NOT NULL DEFAULT 0, link_clicks bigint(20) unsigned NOT NULL DEFAULT 0, outbound_clicks bigint(20) unsigned NOT NULL DEFAULT 0, engagements bigint(20) unsigned NOT NULL DEFAULT 0, landing_page_views bigint(20) unsigned NOT NULL DEFAULT 0, view_content bigint(20) unsigned NOT NULL DEFAULT 0, add_to_cart bigint(20) unsigned NOT NULL DEFAULT 0, initiate_checkout bigint(20) unsigned NOT NULL DEFAULT 0, results_count bigint(20) unsigned NOT NULL DEFAULT 0, purchases bigint(20) unsigned NOT NULL DEFAULT 0, revenue decimal(18,4) NOT NULL DEFAULT 0, video_metrics longtext NULL, products_payload longtext NULL, measurement_metadata longtext NULL, score decimal(6,2) NULL, created_at datetime NOT NULL, PRIMARY KEY (id), KEY result_date (result_date), KEY campaign (campaign), KEY campaign_id (campaign_id), KEY adset_id (adset_id), KEY ad_id (ad_id) ) $charset;"); dbDelta("CREATE TABLE $tasks ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, user_id bigint(20) unsigned NOT NULL DEFAULT 0, title varchar(191) NOT NULL, details text NULL, status varchar(30) NOT NULL DEFAULT 'open', priority varchar(30) NOT NULL DEFAULT 'medium', created_at datetime NOT NULL, completed_at datetime NULL, PRIMARY KEY (id), KEY status (status), KEY priority (priority) ) $charset;"); update_option(self::OPTION_DB_VERSION,self::VERSION,false); } public static function maybe_upgrade(){if(get_option(self::OPTION_DB_VERSION)!==self::VERSION)self::activate();} public static function declare_hpos() { if (class_exists('Automattic\\WooCommerce\\Utilities\\FeaturesUtil')) { Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true); } } private static function capability() { return class_exists('WooCommerce') ? 'manage_woocommerce' : 'manage_options'; } public static function menu() { add_menu_page( 'AdEditors', 'AdEditors', self::capability(), 'adeditors', [__CLASS__, 'render'], 'dashicons-chart-area', 56 ); } public static function is_page() { return is_admin() && isset($_GET['page']) && sanitize_key(wp_unslash($_GET['page'])) === 'adeditors'; } public static function assets($hook) { if (!self::is_page()) return; $url = plugin_dir_url(__FILE__); wp_enqueue_style('adeditors-app', $url . 'assets/css/shopify-app.css', [], self::VERSION); wp_enqueue_style('adeditors-ds', $url . 'assets/css/adeditors-design-system.css', ['adeditors-app'], self::VERSION); wp_enqueue_style('adeditors-components', $url . 'assets/css/adeditors-components.css', ['adeditors-ds'], self::VERSION); wp_enqueue_style('adeditors-pages', $url . 'assets/css/adeditors-page-compositions.css', ['adeditors-components'], self::VERSION); wp_enqueue_style('adeditors-mobile', $url . 'assets/css/adeditors-mobile-responsive.css', ['adeditors-pages'], self::VERSION); wp_enqueue_style('adeditors-visual', $url . 'assets/css/adeditors-visual-system.css', ['adeditors-mobile'], self::VERSION); wp_enqueue_script('adeditors-app', $url . 'assets/js/adeditors-app.js', [], self::VERSION, true); wp_enqueue_script('adeditors-visual', $url . 'assets/js/adeditors-visual-system.js', ['adeditors-app'], self::VERSION, true); wp_localize_script('adeditors-app', 'ADEDITORS_WP', [ 'root' => esc_url_raw(rest_url(self::REST_NS . '/')), 'nonce' => wp_create_nonce('wp_rest'), 'language' => self::language(), 'currency' => function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : get_option('woocommerce_currency', 'USD'), 'wooActive' => class_exists('WooCommerce'), 'remoteConnected' => self::is_connected(), 'connectUrl' => esc_url_raw(self::connect_admin_url()), 'accountUrl' => esc_url_raw(self::BACKEND . '/dashboard/?lang=' . self::language()), 'pricingUrl' => esc_url_raw(self::BACKEND . '/pricing/?lang=' . self::language()), ]); } public static function language() { $saved = get_option(self::OPTION_LANGUAGE, ''); if (in_array($saved, ['ar','en'], true)) return $saved; return strpos((string) get_locale(), 'ar') === 0 ? 'ar' : 'en'; } public static function render() { if (!current_user_can(self::capability())) wp_die(esc_html__('You do not have permission to access this page.', 'adeditors-commerce')); $language = self::language(); $dir = $language === 'ar' ? 'rtl' : 'ltr'; include __DIR__ . '/views/app.php'; } public static function routes() { register_rest_route(self::REST_NS, '/dashboard', ['methods' => WP_REST_Server::READABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'dashboard']]); register_rest_route(self::REST_NS, '/performance', ['methods' => WP_REST_Server::READABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'performance']]); register_rest_route(self::REST_NS, '/commerce', ['methods' => WP_REST_Server::READABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'commerce']]); register_rest_route(self::REST_NS, '/products', ['methods' => WP_REST_Server::READABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'products']]); register_rest_route(self::REST_NS, '/products/(?P\d+)/analysis', ['methods' => WP_REST_Server::READABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'product_analysis']]); register_rest_route(self::REST_NS, '/products/selection', [ ['methods' => WP_REST_Server::READABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'product_selection']], ['methods' => WP_REST_Server::EDITABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'save_product_selection']], ]); register_rest_route(self::REST_NS, '/tasks', [ ['methods' => WP_REST_Server::READABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'tasks']], ['methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'create_task']], ]); register_rest_route(self::REST_NS, '/tasks/(?P\\d+)', ['methods' => WP_REST_Server::EDITABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'update_task']]); register_rest_route(self::REST_NS, '/results', [ ['methods' => WP_REST_Server::READABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'results']], ['methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'create_result']], ]); register_rest_route(self::REST_NS, '/entities', ['methods' => WP_REST_Server::READABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'entities']]); register_rest_route(self::REST_NS, '/entities/(?Pcampaign|adset|ad)', ['methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'create_entity']]); register_rest_route(self::REST_NS, '/results/existing-dates', ['methods' => WP_REST_Server::READABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'existing_result_dates']]); register_rest_route(self::REST_NS, '/daily-message', ['methods' => WP_REST_Server::READABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'daily_message_endpoint']]); register_rest_route(self::REST_NS, '/intelligence', ['methods' => WP_REST_Server::READABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'intelligence']]); register_rest_route(self::REST_NS, '/strategic-planning', ['methods' => WP_REST_Server::READABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'strategic_planning']]); register_rest_route(self::REST_NS, '/strategic-planning/(?Panswers|build|activate|compare|review|targets|course-correction|apply-correction)', ['methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'strategic_planning_action']]); register_rest_route(self::REST_NS, '/settings/language', ['methods' => WP_REST_Server::EDITABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'save_language']]); register_rest_route(self::REST_NS, '/status', ['methods' => WP_REST_Server::READABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'status']]); register_rest_route(self::REST_NS, '/connection/start', ['methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'connection_start']]); register_rest_route(self::REST_NS, '/connection/status', ['methods' => WP_REST_Server::READABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'connection_status']]); register_rest_route(self::REST_NS, '/connection/sync', ['methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'connection_sync'], 'args' => ['scope' => ['type'=>'string','enum'=>['all','products','orders'],'default'=>'all']]]); register_rest_route(self::REST_NS, '/connection/disconnect', ['methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [__CLASS__, 'can_use'], 'callback' => [__CLASS__, 'connection_disconnect']]); } public static function can_use() { return current_user_can(self::capability()); } private static function date_range(WP_REST_Request $r) { $to = sanitize_text_field($r->get_param('to') ?: gmdate('Y-m-d')); $from = sanitize_text_field($r->get_param('from') ?: gmdate('Y-m-d', strtotime('-29 days'))); return [$from, $to]; } private static function woo_orders($from, $to, $limit = -1) { if (!function_exists('wc_get_orders')) return []; return wc_get_orders([ 'limit' => $limit, 'status' => array_keys(wc_get_order_statuses()), 'date_created' => $from . ' 00:00:00...' . $to . ' 23:59:59', 'orderby' => 'date', 'order' => 'DESC', 'return' => 'objects' ]); } private static function order_metrics($orders) { $gross = 0.0; $refunds = 0.0; $count = 0; $completed = 0; foreach ($orders as $order) { if (!is_a($order, 'WC_Order')) continue; $count++; $gross += (float) $order->get_total(); $refunds += abs((float) $order->get_total_refunded()); if (in_array($order->get_status(), ['processing','completed'], true)) $completed++; } $net = max(0, $gross - $refunds); return ['orders'=>$count,'gross_sales'=>$gross,'refunds'=>$refunds,'net_sales'=>$net,'aov'=>$count ? $net/$count : 0,'completed'=>$completed]; } private static function remote_workspace(WP_REST_Request $r) { if (!self::is_connected()) return new WP_Error('not_connected', 'Connect your AdEditors account first.', ['status'=>409]); [$from,$to] = self::date_range($r); $query = http_build_query(['from'=>$from,'to'=>$to]); return self::remote('workspace?' . $query); } public static function dashboard(WP_REST_Request $r) { if (self::is_connected()) { $remote = self::remote_workspace($r); if (!is_wp_error($remote) && !empty($remote['cloud_access']) && is_array($remote['dashboard'] ?? null)) { $dashboard=$remote['dashboard']; $dashboard['source']='adeditors_cloud'; $dashboard['tasks']=(array)($remote['tasks']??[]); $dashboard['recommendations']=(array)($remote['recommendations']??[]); $dashboard['signals']=(array)($remote['signals']??[]); $dashboard['results']=(array)($remote['results']??[]); $dashboard['commerce']=$remote['commerce']??null; $dashboard['generated_at']=$remote['generated_at']??null; return rest_ensure_response($dashboard); } if (is_wp_error($remote) && in_array($remote->get_error_code(), ['token_invalid','token_required'], true)) { delete_option(self::OPTION_TOKEN); delete_option(self::OPTION_CONNECTION_ID); } } [$from,$to] = self::date_range($r); $orders = self::woo_orders($from,$to); $m = self::order_metrics($orders); global $wpdb; $results_table = $wpdb->prefix . 'adeditors_results'; $tasks_table = $wpdb->prefix . 'adeditors_tasks'; $uid=get_current_user_id(); $spend = (float) $wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(spend),0) FROM %i WHERE user_id=%d AND result_date BETWEEN %s AND %s", $results_table, $uid, $from, $to)); $ad_revenue = (float) $wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(revenue),0) FROM %i WHERE user_id=%d AND result_date BETWEEN %s AND %s", $results_table, $uid, $from, $to)); $open_tasks = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM %i WHERE user_id=%d AND status <> 'completed'", $tasks_table, $uid)); $roas = $spend > 0 ? $ad_revenue/$spend : null; $score = self::score($m['net_sales'], $spend, $m['refunds'], $m['gross_sales']); return rest_ensure_response([ 'range'=>compact('from','to'), 'store'=>self::store_info(), 'metrics'=>$m + ['ad_spend'=>$spend,'ad_revenue'=>$ad_revenue,'roas'=>$roas,'score'=>$score,'open_tasks'=>$open_tasks], 'recent_orders'=>self::serialize_orders(array_slice($orders,0,8)), 'message'=>self::daily_message($m,$spend,$roas),'source'=>'woocommerce_local','tasks'=>[],'recommendations'=>[],'signals'=>[],'results'=>[],'commerce'=>null,'generated_at'=>current_time('mysql'), ]); } private static function score($sales,$spend,$refunds,$gross) { $score = 70; if ($sales > 0) $score += 8; if ($spend > 0 && $sales/$spend >= 4) $score += 12; if ($gross > 0 && $refunds/$gross > .10) $score -= 18; return max(0,min(100,$score)); } private static function daily_message($m,$spend,$roas) { $lang=self::language(); if ($m['orders']===0) return $lang==='ar' ? 'لا توجد طلبات في الفترة المحددة. راجع الزيارات والعروض ومصادر الاكتساب.' : 'No orders were recorded in the selected period. Review traffic, offers, and acquisition sources.'; if ($roas !== null && $roas >= 4) return $lang==='ar' ? 'العائد الإعلاني قوي في الفترة الحالية. حافظ على الكفاءة ووسّع أفضل المنتجات تدريجيًا.' : 'Advertising return is strong in the current period. Protect efficiency and scale the strongest products gradually.'; return $lang==='ar' ? 'راجع أداء المبيعات والإنفاق معًا، وركّز على المنتجات والقنوات الأعلى مساهمة في صافي المبيعات.' : 'Review sales and spend together, focusing on products and channels contributing most to net sales.'; } private static function store_info() { return [ 'name'=>get_bloginfo('name'), 'url'=>home_url('/'), 'currency'=>function_exists('get_woocommerce_currency')?get_woocommerce_currency():get_option('woocommerce_currency','USD'), 'woocommerce'=>class_exists('WooCommerce'), 'wp_version'=>get_bloginfo('version'), 'wc_version'=>defined('WC_VERSION')?WC_VERSION:null ]; } private static function serialize_orders($orders) { $out=[]; foreach($orders as $o){ if(!is_a($o,'WC_Order')) continue; $out[]=['id'=>$o->get_id(),'date'=>$o->get_date_created()?$o->get_date_created()->date_i18n('Y-m-d H:i'):'','status'=>$o->get_status(),'total'=>(float)$o->get_total(),'currency'=>$o->get_currency(),'customer'=>trim($o->get_formatted_billing_full_name())]; } return $out; } public static function commerce(WP_REST_Request $r) { if(self::is_connected()){ $remote=self::remote_workspace($r); if(!is_wp_error($remote)&&!empty($remote['cloud_access'])&&is_array($remote['commerce']??null))return rest_ensure_response($remote['commerce']); } [$from,$to]=self::date_range($r); $orders=self::woo_orders($from,$to); $m=self::order_metrics($orders); return rest_ensure_response(['metrics'=>$m,'orders'=>self::serialize_orders(array_slice($orders,0,50)),'products'=>self::product_sales($orders),'funnel'=>['orders'=>$m['orders'],'paid'=>$m['completed'],'refunded_total'=>$m['refunds']]]); } private static function product_sales($orders) { $map=[]; foreach($orders as $order){ if(!is_a($order,'WC_Order')) continue; foreach($order->get_items() as $item){ $pid=$item->get_product_id(); if(!$pid)continue; if(!isset($map[$pid]))$map[$pid]=['id'=>$pid,'name'=>$item->get_name(),'units'=>0,'revenue'=>0.0]; $map[$pid]['units']+=(int)$item->get_quantity(); $map[$pid]['revenue']+=(float)$item->get_total(); }} usort($map,fn($a,$b)=>$b['revenue']<=>$a['revenue']); return array_values($map); } public static function products(WP_REST_Request $r) { if(self::is_connected()){ $remote=self::remote_workspace($r); if(!is_wp_error($remote)&&!empty($remote['cloud_access'])&&is_array($remote['products']??null))return rest_ensure_response($remote['products']); } if(!function_exists('wc_get_products')) return rest_ensure_response(['items'=>[]]); $products=wc_get_products(['limit'=>100,'status'=>['publish','private','draft'],'orderby'=>'date','order'=>'DESC']); $items=[]; foreach($products as $p){$items[]=['id'=>$p->get_id(),'name'=>$p->get_name(),'sku'=>$p->get_sku(),'price'=>(float)$p->get_price(),'regular_price'=>(float)$p->get_regular_price(),'stock_status'=>$p->get_stock_status(),'stock_quantity'=>$p->get_stock_quantity(),'status'=>$p->get_status(),'image'=>wp_get_attachment_image_url($p->get_image_id(),'thumbnail')?:''];} return rest_ensure_response(['items'=>$items]); } public static function product_analysis(WP_REST_Request $r) { $id=absint($r->get_param('id'));if(!$id)return new WP_Error('invalid_product','Invalid product.',['status'=>400]); if(self::is_connected()){[$from,$to]=self::date_range($r);$remote=self::remote('products/'.$id.'/analysis?'.http_build_query(['from'=>$from,'to'=>$to]));if(!is_wp_error($remote))return rest_ensure_response($remote);return$remote;} return new WP_Error('cloud_required','Connect AdEditors Cloud to load product intelligence.',['status'=>409]); } public static function product_selection() { return rest_ensure_response(['product_ids'=>array_values(array_map('absint',(array)get_option(self::OPTION_SELECTED_PRODUCTS,[])))]); } public static function save_product_selection(WP_REST_Request $r) { $payload=$r->get_json_params();$ids=array_values(array_unique(array_filter(array_map('absint',(array)($payload['product_ids']??[]))))); if(count($ids)>2000)return new WP_Error('selection_too_large','The product selection is too large.',['status'=>400]); update_option(self::OPTION_SELECTED_PRODUCTS,$ids,false); return rest_ensure_response(['saved'=>true,'product_ids'=>$ids,'count'=>count($ids)]); } public static function entities(WP_REST_Request $r) { if(self::is_connected()){$remote=self::remote_workspace($r);if(!is_wp_error($remote)&&!empty($remote['cloud_access']))return rest_ensure_response((array)($remote['entities']??[]));} return rest_ensure_response(['campaigns'=>[],'adsets'=>[],'ads'=>[],'products'=>[]]); } public static function create_entity(WP_REST_Request $r) { if(!self::is_connected())return new WP_Error('cloud_connection_required','Connect AdEditors to create advertising entities.',['status'=>409]); $type=sanitize_key($r['type']??'');$path=$type==='campaign'?'campaigns':($type==='adset'?'adsets':($type==='ad'?'ads':'')); if(!$path)return new WP_Error('invalid_entity_type','Invalid entity type.',['status'=>400]); $payload=$r->get_json_params();$remote=self::remote($path,'POST',is_array($payload)?$payload:[]);return is_wp_error($remote)?$remote:rest_ensure_response($remote); } public static function existing_result_dates(WP_REST_Request $r) { if(self::is_connected()){$qs=http_build_query(['campaign_id'=>absint($r->get_param('campaign_id')),'adset_id'=>absint($r->get_param('adset_id')),'ad_id'=>absint($r->get_param('ad_id'))]);$remote=self::remote('results/existing-dates?'.$qs);if(!is_wp_error($remote))return rest_ensure_response($remote);} global $wpdb;$table=$wpdb->prefix.'adeditors_results';$dates=$wpdb->get_col($wpdb->prepare("SELECT DISTINCT result_date FROM %i WHERE user_id=%d AND campaign_id=%d AND adset_id=%d AND ad_id=%d ORDER BY result_date ASC", $table, get_current_user_id(), absint($r->get_param('campaign_id')), absint($r->get_param('adset_id')), absint($r->get_param('ad_id'))))?:[];return rest_ensure_response(['dates'=>$dates]); } public static function daily_message_endpoint(WP_REST_Request $r) { if(self::is_connected()){$remote=self::remote('daily-message');if(!is_wp_error($remote))return rest_ensure_response($remote);}[$from,$to]=self::date_range($r);$orders=self::woo_orders($from,$to);$m=self::order_metrics($orders);return rest_ensure_response(['available'=>true,'state'=>'local','message'=>self::daily_message($m,0,null),'source_system'=>'woocommerce_local']); } public static function performance(WP_REST_Request $r) { return self::results($r); } public static function results(WP_REST_Request $r) { if(self::is_connected()){ $remote=self::remote_workspace($r); if(!is_wp_error($remote)&&!empty($remote['cloud_access']))return rest_ensure_response(['items'=>(array)($remote['results']??[]),'entities'=>(array)($remote['entities']??[]),'source'=>'adeditors_cloud']); } global $wpdb; [$from,$to]=self::date_range($r); $table=$wpdb->prefix.'adeditors_results'; $rows=$wpdb->get_results($wpdb->prepare("SELECT * FROM %i WHERE user_id=%d AND result_date BETWEEN %s AND %s ORDER BY result_date DESC,id DESC LIMIT 500", $table, get_current_user_id(), $from, $to),ARRAY_A); foreach($rows as &$row){$row['roas']=(float)$row['spend']>0?(float)$row['revenue']/(float)$row['spend']:null;$row['product_ids']=json_decode($row['product_ids']?:'[]',true)?:[];$row['products']=json_decode($row['products_payload']??'[]',true)?:[];} return rest_ensure_response(['items'=>$rows]); } public static function create_result(WP_REST_Request $r) { global $wpdb;$p=$r->get_json_params();if(!is_array($p))$p=[];$table=$wpdb->prefix.'adeditors_results'; $date=sanitize_text_field($p['result_date']??'');if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date))return new WP_Error('invalid_date','Invalid result date',['status'=>400]); $campaign_id=absint($p['campaign_id']??0);$adset_id=absint($p['adset_id']??0);$ad_id=absint($p['ad_id']??0);if(self::is_connected()&&(!$campaign_id||!$adset_id||!$ad_id))return new WP_Error('entity_required','Campaign, ad set and ad are required.',['status'=>422]); $numeric=['spend','impressions','reach','clicks','link_clicks','outbound_clicks','engagements','landing_page_views','view_content','add_to_cart','initiate_checkout','purchases','results','revenue','video_views','video_2s_views','video_3s_views','video_6s_views','video_views_25','video_views_50','video_views_75','video_views_95','video_views_100'];foreach($numeric as$k)if(isset($p[$k])&&$p[$k]!==''&&$p[$k]!==null&&(!is_numeric($p[$k])||(float)$p[$k]<0))return new WP_Error('invalid_metric','Metrics cannot be negative.',['status'=>422,'field'=>$k]); $spend=max(0,(float)($p['spend']??0));$revenue=max(0,(float)($p['revenue']??0));$products=is_array($p['products']??null)?$p['products']:[];$product_ids=array_values(array_unique(array_filter(array_map('absint',array_column($products,'product_id')))));if(!$product_ids)$product_ids=array_values(array_filter(array_map('absint',(array)($p['product_ids']??[])))); $remote=null;if(self::is_connected()){$payload=$p;$payload['campaign_id']=$campaign_id;$payload['adset_id']=$adset_id;$payload['ad_id']=$ad_id;$payload['result_date']=$date;$payload['products']=$products;$payload['has_products']=$products?1:0;$payload['source_type']=sanitize_key($p['source_type']??'manual_ad_platform');$payload['measurement_metadata']=is_array($p['measurement_metadata']??null)?$p['measurement_metadata']:['source_type'=>$payload['source_type'],'channel'=>'woocommerce','attribution_scope'=>'ad_platform'];$remote=self::remote('submit','POST',$payload);if(is_wp_error($remote))return$remote;} $campaign=sanitize_text_field($p['campaign']??$p['campaign_name']??'');$adset=sanitize_text_field($p['adset']??$p['adset_name']??'');$ad_name=sanitize_text_field($p['ad_name']??'');$score=$spend>0?min(100,max(0,50+min(40,($revenue/$spend)*5))):null; $video=[];foreach($numeric as$k)if(strpos($k,'video_')===0)$video[$k]=isset($p[$k])?(float)$p[$k]:null; $ok=$wpdb->insert($table,['user_id'=>get_current_user_id(),'result_date'=>$date,'platform'=>sanitize_text_field($p['platform']??''),'campaign'=>$campaign,'campaign_id'=>$campaign_id,'adset'=>$adset,'adset_id'=>$adset_id,'ad_name'=>$ad_name,'ad_id'=>$ad_id,'product_ids'=>wp_json_encode($product_ids),'spend'=>$spend,'impressions'=>absint($p['impressions']??0),'reach'=>absint($p['reach']??0),'clicks'=>absint($p['clicks']??0),'link_clicks'=>absint($p['link_clicks']??0),'outbound_clicks'=>absint($p['outbound_clicks']??0),'engagements'=>absint($p['engagements']??0),'landing_page_views'=>absint($p['landing_page_views']??0),'view_content'=>absint($p['view_content']??0),'add_to_cart'=>absint($p['add_to_cart']??0),'initiate_checkout'=>absint($p['initiate_checkout']??0),'results_count'=>absint($p['results']??$p['purchases']??0),'purchases'=>absint($p['purchases']??0),'revenue'=>$revenue,'video_metrics'=>wp_json_encode($video),'products_payload'=>wp_json_encode($products),'measurement_metadata'=>wp_json_encode($p['measurement_metadata']??[]),'score'=>$score,'created_at'=>current_time('mysql')]); if(!$ok)return new WP_Error('db_error','Could not save local result mirror',['status'=>500]);return rest_ensure_response(['id'=>(int)$wpdb->insert_id,'saved'=>true,'remote_synced'=>self::is_connected(),'remote'=>$remote]); } public static function tasks(WP_REST_Request $r=null) { if(self::is_connected()){ $req=$r instanceof WP_REST_Request?$r:new WP_REST_Request('GET','/'); $remote=self::remote_workspace($req); if(!is_wp_error($remote)&&!empty($remote['cloud_access']))return rest_ensure_response(['items'=>(array)($remote['tasks']??[]),'source'=>'adeditors_cloud']); } global $wpdb;$table=$wpdb->prefix.'adeditors_tasks';$rows=$wpdb->get_results($wpdb->prepare("SELECT * FROM %i WHERE user_id=%d ORDER BY FIELD(priority,'critical','high','medium','low'),created_at DESC LIMIT 200", $table, get_current_user_id()),ARRAY_A);return rest_ensure_response(['items'=>$rows]); } public static function create_task(WP_REST_Request $r){global $wpdb;$p=$r->get_json_params();$table=$wpdb->prefix.'adeditors_tasks';$title=sanitize_text_field($p['title']??'');if($title==='')return new WP_Error('title_required','Title is required',['status'=>400]);$wpdb->insert($table,['user_id'=>get_current_user_id(),'title'=>$title,'details'=>sanitize_textarea_field($p['details']??''),'status'=>'open','priority'=>in_array(($p['priority']??''),['low','medium','high','critical'],true)?$p['priority']:'medium','created_at'=>current_time('mysql')]);return rest_ensure_response(['id'=>(int)$wpdb->insert_id]);} public static function update_task(WP_REST_Request $r){$id=absint($r['id']);$p=$r->get_json_params();$status=in_array(($p['status']??''),['open','in_progress','completed'],true)?$p['status']:'open';if(self::is_connected()&&$status==='completed'){ $remote=self::remote('tasks/'.$id.'/complete','POST',[]); if(!is_wp_error($remote))return rest_ensure_response($remote); }global $wpdb;$wpdb->update($wpdb->prefix.'adeditors_tasks',['status'=>$status,'completed_at'=>$status==='completed'?current_time('mysql'):null],['id'=>$id,'user_id'=>get_current_user_id()]);return rest_ensure_response(['updated'=>true]);} public static function strategic_planning(WP_REST_Request $r) { if (!self::is_connected()) return new WP_Error('cloud_required','Connect AdEditors Cloud to use strategic planning.',['status'=>409]); $remote=self::remote('engines/strategic_planning'); return is_wp_error($remote)?$remote:rest_ensure_response($remote); } public static function strategic_planning_action(WP_REST_Request $r) { if (!self::is_connected()) return new WP_Error('cloud_required','Connect AdEditors Cloud to use strategic planning.',['status'=>409]); $action=sanitize_key($r->get_param('action')); $allowed=['answers','build','activate','compare','review','targets','course-correction','apply-correction']; if(!in_array($action,$allowed,true))return new WP_Error('invalid_action','Invalid strategic planning action.',['status'=>400]); $body=$r->get_json_params();$body=is_array($body)?$body:[]; $remote=self::remote('engines/strategic_planning/'.$action,'POST',$body); return is_wp_error($remote)?$remote:rest_ensure_response($remote); } public static function intelligence(WP_REST_Request $r) { if(self::is_connected()){ $remote=self::remote_workspace($r); if(!is_wp_error($remote)&&!empty($remote['cloud_access'])&&is_array($remote['intelligence']??null))return rest_ensure_response($remote['intelligence']); } [$from,$to]=self::date_range($r);$orders=self::woo_orders($from,$to);$m=self::order_metrics($orders);global $wpdb;$table=$wpdb->prefix.'adeditors_results';$spend=(float)$wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(spend),0) FROM %i WHERE user_id=%d AND result_date BETWEEN %s AND %s", $table, get_current_user_id(), $from, $to));$rev=(float)$wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(revenue),0) FROM %i WHERE user_id=%d AND result_date BETWEEN %s AND %s", $table, get_current_user_id(), $from, $to));$roas=$spend>0?$rev/$spend:null;$lang=self::language(); $planning=[];$success=[];$value=[];$decisions=[]; $planning[]=['type'=>'insight','title'=>$lang==='ar'?'أولوية النمو':'Growth priority','body'=>$lang==='ar'?'ابدأ من المنتجات الأعلى مساهمة في صافي المبيعات قبل توسيع الإنفاق.':'Start with products contributing most to net sales before expanding spend.']; $success[]=['type'=>'information','title'=>$lang==='ar'?'صحة الطلبات':'Order health','body'=>sprintf($lang==='ar'?'تم تسجيل %d طلبًا، بمتوسط قيمة %.2f.':'%d orders were recorded with an average value of %.2f.',$m['orders'],$m['aov'])]; $value[]=['type'=>$roas!==null&&$roas>=4?'positive':'warning','title'=>$lang==='ar'?'قيمة الإنفاق':'Spend value','body'=>$roas===null?($lang==='ar'?'أضف نتائج الإعلانات لقياس ROAS وربط الإنفاق بالمبيعات.':'Add advertising results to measure ROAS and connect spend to sales.'):sprintf('ROAS %.2f×',$roas)]; $decisions[]=['type'=>'recommendation','title'=>$lang==='ar'?'قرار مقترح':'Suggested decision','body'=>$m['refunds']>($m['gross_sales']*.1)?($lang==='ar'?'نسبة المرتجعات مرتفعة؛ راجع أسباب الإلغاء والمنتجات المتكررة في المرتجعات.':'Refunds are elevated; review cancellation reasons and products repeatedly refunded.'):($lang==='ar'?'راقب صافي المبيعات والإنفاق معًا قبل أي زيادة كبيرة في الميزانية.':'Track net sales and spend together before any major budget increase.')]; return rest_ensure_response(compact('planning','success','value','decisions')); } public static function save_language(WP_REST_Request $r){$p=$r->get_json_params();$lang=sanitize_key($p['language']??'');if(!in_array($lang,['ar','en'],true))return new WP_Error('invalid_language','Invalid language',['status'=>400]);update_option(self::OPTION_LANGUAGE,$lang,false);return rest_ensure_response(['language'=>$lang]);} private static function site_origin(){ $p=wp_parse_url(home_url('/')); return strtolower($p['scheme'].'://'.$p['host'].(!empty($p['port'])?':'.$p['port']:'')); } public static function is_connected(){return (string)get_option(self::OPTION_TOKEN,'')!=='';} public static function allowed_redirect_hosts($hosts) { $backend_host = wp_parse_url(self::BACKEND, PHP_URL_HOST); if ($backend_host && !in_array($backend_host, $hosts, true)) { $hosts[] = $backend_host; } return $hosts; } public static function connect_admin_url(){ return wp_nonce_url(admin_url('admin.php?page=adeditors&adeditors_action=connect'), 'adeditors_connect', 'adeditors_nonce'); } public static function handle_admin_action(){ if (!is_admin() || empty($_GET['page']) || sanitize_key(wp_unslash($_GET['page'])) !== 'adeditors') return; $action = sanitize_key(wp_unslash($_GET['adeditors_action'] ?? '')); if ($action !== 'connect') return; if (!current_user_can(self::capability())) wp_die(esc_html__('You do not have permission to connect this store.', 'adeditors-commerce')); $nonce = sanitize_text_field(wp_unslash($_GET['adeditors_nonce'] ?? '')); if (!wp_verify_nonce($nonce, 'adeditors_connect')) wp_die(esc_html__('The connection request could not be verified. Please try again.', 'adeditors-commerce')); $state=wp_generate_password(40,false,false); update_option(self::OPTION_PAIR_STATE,$state,false); $url=add_query_arg(['site_url'=>self::site_origin(),'return_url'=>self::callback_url(),'state'=>$state,'lang'=>self::language()],self::BACKEND.'/?adpai_woocommerce_connect=1'); add_filter('allowed_redirect_hosts', [__CLASS__, 'allowed_redirect_hosts']); wp_safe_redirect(esc_url_raw($url)); remove_filter('allowed_redirect_hosts', [__CLASS__, 'allowed_redirect_hosts']); exit; } private static function callback_url(){return admin_url('admin.php?page=adeditors');} public static function connection_start(){ $state=wp_generate_password(40,false,false);update_option(self::OPTION_PAIR_STATE,$state,false); $url=add_query_arg(['site_url'=>self::site_origin(),'return_url'=>self::callback_url(),'state'=>$state,'lang'=>self::language()],self::BACKEND.'/?adpai_woocommerce_connect=1'); return rest_ensure_response(['url'=>esc_url_raw($url)]); } public static function handle_pair_callback(){ if(!isset($_GET['page'])||sanitize_key(wp_unslash($_GET['page']))!=='adeditors'||empty($_GET['adeditors_pair_code']))return; if(!current_user_can(self::capability()))return; $state=sanitize_text_field(wp_unslash($_GET['adeditors_state']??''));$saved=(string)get_option(self::OPTION_PAIR_STATE,''); if(!$saved||!hash_equals($saved,$state)){add_settings_error('adeditors','pair_state','AdEditors connection could not be verified.','error');return;} $res=wp_remote_post(self::BACKEND.'/wp-json/adpai-woocommerce/v1/pair/exchange',['timeout'=>20,'headers'=>['Content-Type'=>'application/json'],'body'=>wp_json_encode(['site_url'=>self::site_origin(),'site_name'=>get_bloginfo('name'),'state'=>$state,'code'=>sanitize_text_field(wp_unslash($_GET['adeditors_pair_code']))])]); delete_option(self::OPTION_PAIR_STATE); if(is_wp_error($res)){add_settings_error('adeditors','pair_exchange',$res->get_error_message(),'error');return;} $body=json_decode(wp_remote_retrieve_body($res),true);if(wp_remote_retrieve_response_code($res)>=300||empty($body['token'])){add_settings_error('adeditors','pair_exchange',sanitize_text_field($body['message']??'Connection failed.'),'error');return;} update_option(self::OPTION_TOKEN,sanitize_text_field($body['token']),false);update_option(self::OPTION_CONNECTION_ID,absint($body['connection_id']??0),false); wp_safe_redirect(remove_query_arg(['adeditors_pair_code','adeditors_state'],self::callback_url()));exit; } private static function remote($path,$method='GET',$body=null){ $token=(string)get_option(self::OPTION_TOKEN,'');if(!$token)return new WP_Error('not_connected','Connect your AdEditors account first.',['status'=>409]); $args=['method'=>$method,'timeout'=>30,'headers'=>['Authorization'=>'Bearer '.$token,'Content-Type'=>'application/json','Accept'=>'application/json']];if($body!==null)$args['body']=wp_json_encode($body); $res=wp_remote_request(self::BACKEND.'/wp-json/adpai-woocommerce/v1/'.ltrim($path,'/'),$args); if(is_wp_error($res))return new WP_Error('backend_unreachable',$res->get_error_message(),['status'=>502]); $raw=wp_remote_retrieve_body($res);$data=json_decode($raw,true);$code=wp_remote_retrieve_response_code($res); if($code>=300)return new WP_Error(sanitize_key(is_array($data)?($data['code']??'remote_error'):'remote_error'),sanitize_text_field(is_array($data)?($data['message']??'AdEditors backend request failed.'):'AdEditors backend request failed.'),['status'=>$code]); if(!is_array($data)||json_last_error()!==JSON_ERROR_NONE)return new WP_Error('invalid_backend_response','AdEditors returned an invalid response. Please retry, then review the server PHP error log if the issue continues.',['status'=>502]); return$data; } public static function connection_status(){if(!self::is_connected())return rest_ensure_response(['connected'=>false]);$r=self::remote('status');if(is_wp_error($r)){if(in_array($r->get_error_code(),['token_invalid','token_required'],true)){delete_option(self::OPTION_TOKEN);delete_option(self::OPTION_CONNECTION_ID);}return$r;}return rest_ensure_response($r);} private static function sync_products_payload(){if(!function_exists('wc_get_products'))return[];$out=[];$items=wc_get_products(['limit'=>200,'status'=>['publish','private','draft'],'orderby'=>'date','order'=>'DESC']);foreach($items as$p){$cats=wp_get_post_terms($p->get_id(),'product_cat',['fields'=>'names']);$out[]=['id'=>(string)$p->get_id(),'name'=>$p->get_name(),'category'=>is_wp_error($cats)?'':implode(', ',$cats),'description'=>wp_strip_all_tags($p->get_description()),'url'=>$p->get_permalink(),'image'=>wp_get_attachment_image_url($p->get_image_id(),'full')?:'','price'=>(float)$p->get_price(),'regular_price'=>(float)$p->get_regular_price(),'sku'=>$p->get_sku(),'currency'=>get_woocommerce_currency(),'stock_status'=>$p->get_stock_status(),'stock_quantity'=>$p->get_stock_quantity(),'status'=>$p->get_status()];}return$out;} private static function sync_orders_payload(){if(!function_exists('wc_get_orders'))return[];$after=gmdate('Y-m-d H:i:s',strtotime('-90 days'));$orders=wc_get_orders(['limit'=>200,'status'=>array_keys(wc_get_order_statuses()),'date_created'=>$after.'...'.gmdate('Y-m-d H:i:s'),'orderby'=>'date','order'=>'DESC']);$out=[];foreach($orders as$o){$items=[];foreach($o->get_items()as$i){$prod=$i->get_product();$items[]=['id'=>(string)$i->get_id(),'product_id'=>(string)$i->get_product_id(),'title'=>$i->get_name(),'sku'=>$prod?$prod->get_sku():'','quantity'=>(int)$i->get_quantity(),'gross_total'=>(float)$i->get_total()+(float)$i->get_total_tax(),'net_total'=>(float)$i->get_total()];}$out[]=['id'=>(string)$o->get_id(),'number'=>$o->get_order_number(),'date'=>$o->get_date_created()?$o->get_date_created()->date('Y-m-d H:i:s'):'','currency'=>$o->get_currency(),'gross_total'=>(float)$o->get_total(),'subtotal'=>(float)$o->get_subtotal(),'discounts'=>(float)$o->get_discount_total(),'refunds'=>abs((float)$o->get_total_refunded()),'net_total'=>max(0,(float)$o->get_total()-abs((float)$o->get_total_refunded())),'shipping'=>(float)$o->get_shipping_total(),'requires_shipping'=>true,'financial_status'=>$o->get_status(),'fulfillment_status'=>$o->get_status(),'items'=>$items];}return$out;} public static function connection_sync(WP_REST_Request $request){if(!class_exists('WooCommerce'))return new WP_Error('woocommerce_required','WooCommerce must be active.',['status'=>409]);$scope=sanitize_key($request->get_param('scope')?:'all');if(!in_array($scope,['all','products','orders'],true))$scope='all';$payload=['products'=>[],'orders'=>[]];if(in_array($scope,['all','products'],true))$payload['products']=self::sync_products_payload();if(in_array($scope,['all','orders'],true))$payload['orders']=self::sync_orders_payload();$r=self::remote('sync','POST',$payload);if(is_wp_error($r))return$r;update_option(self::OPTION_LAST_SYNC,current_time('mysql'),false);$r['scope']=$scope;return rest_ensure_response($r);} public static function connection_disconnect(){if(self::is_connected())self::remote('disconnect','POST',[]);delete_option(self::OPTION_TOKEN);delete_option(self::OPTION_CONNECTION_ID);delete_option(self::OPTION_LAST_SYNC);return rest_ensure_response(['connected'=>false]);} public static function status(){ $payload=['wordpress'=>true,'woocommerce'=>class_exists('WooCommerce'),'store'=>self::store_info(),'language'=>self::language(),'connection'=>['connected'=>self::is_connected(),'connection_id'=>absint(get_option(self::OPTION_CONNECTION_ID,0)),'last_sync_at'=>get_option(self::OPTION_LAST_SYNC,'')],'cloud_access'=>false,'account'=>null,'business_profile'=>null,'subscription'=>['plan'=>'free','plan_code'=>'free','entitlement_level'=>'free','status'=>'active']]; if(self::is_connected()){ $req=new WP_REST_Request('GET','/'); $remote=self::remote_workspace($req); if(is_wp_error($remote)){ if(in_array($remote->get_error_code(),['token_invalid','token_required'],true)){delete_option(self::OPTION_TOKEN);delete_option(self::OPTION_CONNECTION_ID);$payload['connection']['connected']=false;} $payload['remote_error']=['code'=>$remote->get_error_code(),'message'=>$remote->get_error_message()]; } else { $payload['cloud_access']=!empty($remote['cloud_access']);$payload['brand_icon_url']=$remote['brand_icon_url']??'';$payload['brand_icon_data_uri']=$remote['brand_icon_data_uri']??'';$payload['account']=$remote['account']??null;$payload['business_profile']=$remote['business_profile']??null;$payload['subscription']=$remote['subscription']??$payload['subscription'];$payload['connection']=array_merge($payload['connection'],['connected'=>true,'connection_id'=>absint($remote['connection']['id']??$payload['connection']['connection_id']),'last_sync_at'=>$remote['connection']['last_sync_at']??$payload['connection']['last_sync_at']]); } } return rest_ensure_response($payload); } } register_activation_hook(__FILE__, ['AdEditors_Commerce_Intelligence','activate']); AdEditors_Commerce_Intelligence::init();