prefix . 'aieo_push_dispatch'; $blank = array( 'available' => false, 'model' => $model, 'window_days' => $window, 'campaigns' => array(), 'note' => __( 'No attributable order paths in this window yet.', 'ai-eshop-optimizer' ) ); if ( ! self::table_exists( $disp ) ) { return $blank; } $orders = self::orders_subquery(); if ( '' === $orders ) { return $blank; } $rows = $wpdb->get_results( $wpdb->prepare( "SELECT o.id AS order_id, o.total_amount AS total, o.date_created_gmt AS odate, d.trigger_id AS campaign, d.channel AS channel, d.sent_at AS tdate FROM {$orders} o JOIN `{$disp}` d ON ( ( o.customer_id > 0 AND d.user_id = o.customer_id ) OR ( o.billing_email <> '' AND d.recipient_email = o.billing_email COLLATE utf8mb4_general_ci ) ) AND d.channel IN ('email','push') AND ( d.state IN ('clicked','converted') OR d.opened_at IS NOT NULL ) AND d.sent_at IS NOT NULL AND d.sent_at <= o.date_created_gmt AND d.sent_at >= ( o.date_created_gmt - INTERVAL %d DAY ) WHERE o.date_created_gmt >= ( UTC_TIMESTAMP() - INTERVAL %d DAY ) ORDER BY o.id ASC, d.sent_at ASC LIMIT " . (int) self::MAX_ROWS, $window, $lookback ), ARRAY_A ); if ( empty( $rows ) ) { return $blank; } $paths = array(); foreach ( $rows as $r ) { $oid = (int) $r['order_id']; if ( ! isset( $paths[ $oid ] ) ) { $paths[ $oid ] = array( 'total' => (float) $r['total'], 'odate' => (string) $r['odate'], 'touches' => array() ); } $paths[ $oid ]['touches'][] = array( 'campaign' => (string) $r['campaign'], 'channel' => (string) $r['channel'], 'tdate' => (string) $r['tdate'] ); } $credit = array(); $orders_n = 0; $total_credited = 0.0; foreach ( $paths as $p ) { $weights = self::weights( $p['touches'], $p['odate'], $model ); $orders_n++; foreach ( $p['touches'] as $i => $tp ) { $c = $tp['campaign']; if ( ! isset( $credit[ $c ] ) ) { $credit[ $c ] = array( 'campaign' => $c, 'credited_revenue' => 0.0, 'touches' => 0 ); } $share = $p['total'] * ( isset( $weights[ $i ] ) ? $weights[ $i ] : 0 ); $credit[ $c ]['credited_revenue'] += $share; $credit[ $c ]['touches']++; $total_credited += $share; } } $out = array_values( $credit ); foreach ( $out as &$c ) { $c['credited_revenue'] = round( $c['credited_revenue'], 2 ); $c['label'] = self::campaign_label( $c['campaign'] ); } unset( $c ); usort( $out, function ( $a, $b ) { return $b['credited_revenue'] <=> $a['credited_revenue']; } ); return array( 'available' => true, 'model' => $model, 'models' => self::MODELS, 'window_days' => $window, 'lookback_days' => $lookback, 'orders_with_path' => $orders_n, 'total_credited' => round( $total_credited, 2 ), 'campaigns' => $out, 'note' => __( 'Credit is spread across every OWNED message the customer engaged with before buying. last_click = 100% to the final touch; linear = equal; time_decay = more to recent touches (7-day half-life); position = 40/40 first+last, 20% shared between. Compare models to see which campaigns last-click under-credits.', 'ai-eshop-optimizer' ), ); } private static function weights( $touches, $order_date, $model ) { $n = count( $touches ); if ( $n <= 0 ) { return array(); } if ( 1 === $n ) { return array( 1.0 ); } switch ( $model ) { case 'last_click': $w = array_fill( 0, $n, 0.0 ); $w[ $n - 1 ] = 1.0; return $w; case 'linear': return array_fill( 0, $n, 1.0 / $n ); case 'position': if ( 2 === $n ) { return array( 0.5, 0.5 ); } $w = array_fill( 0, $n, 0.0 ); $w[0] = 0.4; $w[ $n - 1 ] = 0.4; $mid = $n - 2; for ( $i = 1; $i < $n - 1; $i++ ) { $w[ $i ] = 0.2 / $mid; } return $w; case 'time_decay': default: $ot = strtotime( (string) $order_date . ' UTC' ); $raw = array(); $sum = 0.0; foreach ( $touches as $tp ) { $tt = strtotime( (string) $tp['tdate'] . ' UTC' ); $days = max( 0, ( $ot - $tt ) / DAY_IN_SECONDS ); $wt = pow( 2, - $days / 7.0 ); $raw[] = $wt; $sum += $wt; } if ( $sum <= 0 ) { return array_fill( 0, $n, 1.0 / $n ); } return array_map( function ( $x ) use ( $sum ) { return $x / $sum; }, $raw ); } } private static function campaign_label( $campaign ) { if ( 0 === strpos( $campaign, 'wf_' ) && class_exists( 'AIEO_Workflows' ) ) { $wf = AIEO_Workflows::get( (int) substr( $campaign, 3 ) ); if ( $wf ) { return (string) $wf['name']; } } if ( 0 === strpos( $campaign, 'campaign:' ) ) { return __( 'Broadcast', 'ai-eshop-optimizer' ) . ' #' . substr( $campaign, 9 ); } return $campaign; } private static function orders_subquery() { global $wpdb; $hpos = $wpdb->prefix . 'wc_orders'; if ( self::table_exists( $hpos ) ) { return "(SELECT id, customer_id, billing_email, date_created_gmt, total_amount FROM `{$hpos}` WHERE type = 'shop_order' AND status IN ('wc-completed','wc-processing'))"; } $posts = $wpdb->posts; $meta = $wpdb->postmeta; if ( ! self::table_exists( $posts ) ) { return ''; } return "(SELECT p.ID AS id, CAST(COALESCE(cu.meta_value, 0) AS UNSIGNED) AS customer_id, COALESCE(be.meta_value, '') AS billing_email, p.post_date_gmt AS date_created_gmt, CAST(COALESCE(tot.meta_value, 0) AS DECIMAL(14,2)) AS total_amount FROM `{$posts}` p LEFT JOIN `{$meta}` cu ON cu.post_id = p.ID AND cu.meta_key = '_customer_user' LEFT JOIN `{$meta}` be ON be.post_id = p.ID AND be.meta_key = '_billing_email' LEFT JOIN `{$meta}` tot ON tot.post_id = p.ID AND tot.meta_key = '_order_total' WHERE p.post_type = 'shop_order' AND p.post_status IN ('wc-completed','wc-processing'))"; } private static function table_exists( $t ) { global $wpdb; return (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) === $t; } }