$kind, 'at' => time(), 'by' => (int) $uid );
} else {
return false;
}
if ( empty( $all ) ) {
delete_option( self::OPTION_DECISIONS );
} else {
update_option( self::OPTION_DECISIONS, $all, false );
}
return true;
}
public static function decide_set( $stage_id, $set_id, $kind, $uid ) {
$stage_id = sanitize_key( (string) $stage_id );
$set_id = sanitize_key( (string) $set_id );
$kind = sanitize_key( (string) $kind );
if ( ! in_array( $kind, array( 'not_applicable', 'undo' ), true ) ) {
return 0;
}
$n = 0;
foreach ( self::stages() as $stage ) {
if ( $stage['id'] !== $stage_id || empty( $stage['na_sets'][ $set_id ] ) ) {
continue;
}
foreach ( $stage['steps'] as $step ) {
if ( isset( $step['na_set'] ) && $step['na_set'] === $set_id && self::decide( $step['id'], $kind, $uid ) ) {
$n++;
}
}
}
return $n;
}
public static function stamp_verify( array $result ) {
$s = isset( $result['summary'] ) && is_array( $result['summary'] ) ? $result['summary'] : array();
update_option( self::OPTION_VERIFY_LAST, array(
'at' => time(),
'checked' => isset( $s['checked'] ) ? (int) $s['checked'] : 0,
'mismatches' => isset( $s['mismatches'] ) ? (int) $s['mismatches'] : 0,
'canon_drift' => isset( $s['canon_drift'] ) ? (int) $s['canon_drift'] : 0,
), false );
}
public static function evaluate( $stage_ids = null ) {
$decisions = self::decisions();
$out = array();
foreach ( self::stages() as $stage ) {
if ( is_array( $stage_ids ) && ! in_array( $stage['id'], $stage_ids, true ) ) {
continue;
}
$steps = array();
$done = 0;
$total = 0;
$hidden = 0;
foreach ( $stage['steps'] as $step ) {
$r = array( 'state' => self::STATE_OPEN, 'detail' => '' );
if ( is_callable( $step['detect'] ) ) {
$got = call_user_func( $step['detect'] );
if ( is_array( $got ) ) {
$r = array_merge( $r, $got );
}
}
$decision = isset( $decisions[ $step['id'] ]['kind'] ) ? (string) $decisions[ $step['id'] ]['kind'] : '';
if ( 'not_applicable' === $decision && self::STATE_SATISFIED !== $r['state'] ) {
$r['state'] = self::STATE_NA;
}
$row = array(
'id' => $step['id'],
'label' => $step['label'],
'state' => $r['state'],
'detail' => (string) $r['detail'],
'action_url' => self::action_url( isset( $step['action'] ) ? $step['action'] : null ),
'action_label' => isset( $step['action']['label'] ) ? (string) $step['action']['label'] : '',
'dark' => isset( $step['dark'] ) ? (string) $step['dark'] : '',
'tier' => self::tier_for( isset( $step['action']['tab'] ) ? $step['action']['tab'] : '' ),
'decision' => $decision,
'na_allowed' => ! empty( $step['na'] ),
'na_set' => isset( $step['na_set'] ) ? (string) $step['na_set'] : '',
'urgent' => ! empty( $step['urgent'] ) && self::STATE_OPEN === $r['state'],
);
if ( 'dismissed' === $decision && self::STATE_SATISFIED !== $row['state'] ) {
$hidden++;
}
if ( self::STATE_NA !== $row['state'] ) {
$total++;
if ( self::STATE_SATISFIED === $row['state'] ) {
$done++;
}
}
$steps[] = $row;
}
$sets = array();
foreach ( (array) ( isset( $stage['na_sets'] ) ? $stage['na_sets'] : array() ) as $set_id => $set ) {
$ids = array();
$applied = 0;
foreach ( $steps as $row ) {
if ( $row['na_set'] !== (string) $set_id ) {
continue;
}
$ids[] = $row['id'];
if ( 'not_applicable' === $row['decision'] ) {
$applied++;
}
}
if ( empty( $ids ) ) {
continue;
}
$sets[ (string) $set_id ] = array(
'id' => (string) $set_id,
'label' => (string) $set['label'],
'undo_label' => (string) $set['undo'],
'steps' => $ids,
'applied' => $applied,
'total' => count( $ids ),
);
}
$out[] = array(
'id' => $stage['id'],
'label' => $stage['label'],
'question' => $stage['question'],
'steps' => $steps,
'done' => $done,
'total' => $total,
'hidden' => $hidden,
'na_sets' => $sets,
);
}
return $out;
}
public static function strip_for_user( $uid ) {
if ( ! class_exists( 'AIEO_Shop_Shape' ) || 'blank' === AIEO_Shop_Shape::screen() ) {
return null;
}
$stages = self::evaluate();
$snap = get_user_meta( (int) $uid, self::META_STRIP_DISMISSED, true );
if ( is_array( $snap ) && isset( $snap['done'] ) && is_array( $snap['done'] ) ) {
$regressed = false;
foreach ( $stages as $s ) {
if ( isset( $snap['done'][ $s['id'] ] ) && (int) $s['done'] < (int) $snap['done'][ $s['id'] ] ) {
$regressed = true;
break;
}
}
if ( ! $regressed ) {
return null;
}
delete_user_meta( (int) $uid, self::META_STRIP_DISMISSED );
}
$pick = null;
foreach ( $stages as $s ) {
if ( $s['total'] < 1 || $s['done'] >= $s['total'] ) {
continue;
}
$ratio = $s['done'] / $s['total'];
if ( null === $pick || $ratio < $pick['ratio'] ) {
$pick = array( 'ratio' => $ratio, 'stage' => $s );
}
}
if ( null === $pick ) {
return null;
}
$url = class_exists( 'AIEO_DMM' ) ? AIEO_DMM::instance()->get_subtab_url( 'getting_set_up' ) . '#aieo-gsu-stage-' . $pick['stage']['id'] : '';
return array(
'stage' => (string) $pick['stage']['id'],
'label' => (string) $pick['stage']['label'],
'done' => (int) $pick['stage']['done'],
'total' => (int) $pick['stage']['total'],
'url' => $url,
);
}
public static function dismiss_strip( $uid ) {
$done = array();
foreach ( self::evaluate() as $s ) {
$done[ $s['id'] ] = (int) $s['done'];
}
update_user_meta( (int) $uid, self::META_STRIP_DISMISSED, array( 'at' => time(), 'done' => $done ) );
}
public static function base_country() {
$raw = (string) get_option( 'woocommerce_default_country', '' );
$code = strtoupper( (string) explode( ':', $raw )[0] );
$name = $code;
$eu = in_array( $code, self::EU_COUNTRIES, true );
if ( function_exists( 'WC' ) && WC() && isset( WC()->countries ) && is_object( WC()->countries ) ) {
$all = (array) WC()->countries->get_countries();
if ( isset( $all[ $code ] ) ) {
$name = (string) $all[ $code ];
}
if ( method_exists( WC()->countries, 'get_european_union_countries' ) ) {
$eu = in_array( $code, (array) WC()->countries->get_european_union_countries(), true );
}
}
return array( 'code' => $code, 'name' => $name, 'eu' => $eu );
}
public static function withdrawal_notice_due() {
if ( get_option( self::OPT_WITHDRAWAL_NOTICE_DISMISSED ) ) {
return null;
}
if ( ! class_exists( 'AIEO_Withdrawal' ) || ! method_exists( 'AIEO_Withdrawal', 'is_enabled' ) || AIEO_Withdrawal::is_enabled() ) {
return null;
}
$c = self::base_country();
if ( ! $c['eu'] ) {
return null;
}
$orders = function_exists( 'wc_orders_count' ) ? (int) wc_orders_count( 'completed' ) : 0;
if ( $orders < 1 ) {
return null;
}
return array( 'country' => $c['name'], 'orders' => $orders );
}
public static function render_withdrawal_notice() {
$cap = defined( 'AIEO_CAPABILITY' ) ? AIEO_CAPABILITY : 'manage_options';
if ( ! current_user_can( $cap ) ) {
return;
}
$due = self::withdrawal_notice_due();
if ( null === $due ) {
return;
}
$url = class_exists( 'AIEO_DMM' ) ? AIEO_DMM::instance()->get_subtab_url( 'withdrawal' ) : '';
echo '
'
. esc_html__( 'Shops based in the EU must offer a withdrawal button', 'ai-eshop-optimizer' )
. '
'
. esc_html( sprintf(
__( 'Directive (EU) 2023/2673 requires an online shop to let a consumer withdraw from a distance contract through a button on the site. This shop is based in %1$s and has %2$s completed orders, and the withdrawal-right button is switched off. Rooster One has one; switching it on adds the button, the request form and the queue that handles it.', 'ai-eshop-optimizer' ),
$due['country'],
number_format_i18n( $due['orders'] )
) )
. '
';
if ( '' !== $url ) {
echo '' . esc_html__( 'Open Withdrawal', 'ai-eshop-optimizer' ) . ' ';
}
echo ''
. esc_html__( 'Understood, do not show this again', 'ai-eshop-optimizer' ) . '
';
}
public static function handle_dismiss_withdrawal_notice() {
$cap = defined( 'AIEO_CAPABILITY' ) ? AIEO_CAPABILITY : 'manage_options';
if ( ! current_user_can( $cap ) ) {
wp_die( esc_html__( 'Insufficient permissions.', 'ai-eshop-optimizer' ) );
}
check_admin_referer( 'aieo_dismiss_withdrawal', 'aieo_nonce' );
update_option( self::OPT_WITHDRAWAL_NOTICE_DISMISSED, time(), false );
wp_safe_redirect( wp_get_referer() ?: admin_url() );
exit;
}
public static function identity_preview() {
$name = (string) self::setting( 'seo.org_name', '' );
$logo = (string) self::setting( 'seo.org_logo', '' );
if ( '' === $logo ) {
$icon = (int) get_option( 'site_icon' );
if ( $icon ) {
$src = wp_get_attachment_image_src( $icon, 'full' );
$logo = $src ? (string) $src[0] : '';
}
}
if ( '' === $name && '' === $logo ) {
return null;
}
$shown = '' !== $name ? $name : (string) get_bloginfo( 'name' );
$home = trailingslashit( home_url() );
$org = array(
'@type' => ( 'person' === (string) self::setting( 'seo.org_represents', 'organization' ) ) ? 'Person' : 'Organization',
'@id' => $home . '#organization',
'name' => $shown,
'url' => $home,
);
if ( '' !== $logo ) {
$org['logo'] = array( '@type' => 'ImageObject', 'url' => $logo );
}
$same = self::setting( 'seo.social_urls', array() );
$same = is_array( $same ) ? array_values( array_filter( array_map( 'trim', array_map( 'strval', $same ) ) ) ) : array();
if ( $same ) {
$org['sameAs'] = $same;
}
$phone = (string) self::setting( 'seo.org_phone', '' );
$email = (string) self::setting( 'seo.org_email', '' );
if ( '' !== $phone ) {
$org['telephone'] = $phone;
}
if ( '' !== $email ) {
$org['email'] = $email;
}
$street = (string) self::setting( 'seo.org_street', '' );
$city = (string) self::setting( 'seo.org_locality', '' );
$post = (string) self::setting( 'seo.org_postcode', '' );
$ctry = (string) self::setting( 'seo.org_country', '' );
if ( '' === $street && '' === $city ) {
$street = trim( (string) get_option( 'woocommerce_store_address', '' ) . ' ' . (string) get_option( 'woocommerce_store_address_2', '' ) );
$city = (string) get_option( 'woocommerce_store_city', '' );
$post = (string) get_option( 'woocommerce_store_postcode', '' );
$ctry = self::base_country()['code'];
}
$addr = array_filter( array(
'streetAddress' => trim( $street ),
'addressLocality' => trim( $city ),
'postalCode' => trim( $post ),
'addressCountry' => trim( $ctry ),
), static function ( $v ) { return '' !== $v; } );
if ( $addr ) {
$org['address'] = array_merge( array( '@type' => 'PostalAddress' ), $addr );
}
$lines = array_values( array_filter( array( trim( $street ), trim( $post . ' ' . $city ), trim( $ctry ) ) ) );
return array(
'name' => $shown,
'logo' => $logo,
'tagline' => (string) get_bloginfo( 'description' ),
'host' => (string) wp_parse_url( $home, PHP_URL_HOST ),
'phone' => $phone,
'email' => $email,
'address' => $lines,
'same_as' => $same,
'jsonld' => $org,
);
}
public static function step_exists( $step_id ) {
foreach ( self::stages() as $stage ) {
foreach ( $stage['steps'] as $step ) {
if ( $step['id'] === $step_id ) {
return true;
}
}
}
return false;
}
public static function cogs_coverage() {
global $wpdb;
$hit = get_transient( self::TRANSIENT_COGS );
if ( is_array( $hit ) && isset( $hit['with_cost'], $hit['products'] ) ) {
return $hit;
}
$products = 0;
if ( post_type_exists( 'product' ) ) {
$c = wp_count_posts( 'product' );
$products = ( $c && isset( $c->publish ) ) ? (int) $c->publish : 0;
}
$with = 0;
if ( $products > 0 ) {
$with = (int) $wpdb->get_var(
"SELECT COUNT(DISTINCT pm.post_id)
FROM {$wpdb->postmeta} pm
JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE pm.meta_key = '_cogs_value'
AND pm.meta_value + 0 > 0
AND p.post_type = 'product'
AND p.post_status = 'publish'"
);
}
$out = array( 'with_cost' => $with, 'products' => $products );
set_transient( self::TRANSIENT_COGS, $out, 10 * MINUTE_IN_SECONDS );
return $out;
}
public static function running_now() {
$s = static function ( $key, $default = null ) {
return class_exists( 'AIEO_DMM_Settings' ) ? AIEO_DMM_Settings::get( $key, $default ) : $default;
};
$badges_on = false;
foreach ( (array) get_option( 'aieo_dmm_badges_settings', array() ) as $f ) {
if ( is_array( $f ) && ! empty( $f['enabled'] ) ) {
$badges_on = true;
break;
}
}
$wish = get_option( 'aieo_wishlist_display', array() );
$wish_on = is_array( $wish ) && ! empty( $wish ) && ( ( $wish['show_on_single'] ?? '' ) === 'yes' || ( $wish['show_on_archive'] ?? '' ) === 'yes' );
$conv = get_option( 'aieo_dmm_conversions', array() );
$conv_on = false;
if ( is_array( $conv ) ) {
foreach ( $conv as $k => $v ) {
if ( is_scalar( $v ) && '' !== trim( (string) $v ) && preg_match( '/_id$|pixel|measurement|token/i', (string) $k ) ) {
$conv_on = true;
break;
}
}
}
$consent = get_option( 'aieo_consent_settings', array() );
$trust = get_option( 'aieo_dmm_trust_badges_settings', array() );
$wait = get_option( 'aieo_dmm_waitlist', array() );
$rows = array(
array( 'id' => 'swatches', 'tab' => 'swatches', 'kind' => 'storefront', 'what' => __( 'Colour and size swatches on product cards and product pages', 'ai-eshop-optimizer' ), 'on' => ( 'yes' === (string) get_option( 'aieo_dmm_swatches_runtime_enabled', 'no' ) ) ),
array( 'id' => 'badges', 'tab' => 'badges', 'kind' => 'storefront', 'what' => __( 'Product badges such as New, Sale and Low stock', 'ai-eshop-optimizer' ), 'on' => $badges_on ),
array( 'id' => 'side_cart', 'tab' => 'side_cart', 'kind' => 'storefront', 'what' => __( 'The side cart and its trigger button', 'ai-eshop-optimizer' ), 'on' => (bool) $s( 'side_cart_show_cart_btn', true ) ),
array( 'id' => 'trust_badges', 'tab' => 'trust_badges', 'kind' => 'storefront', 'what' => __( 'Trust badges on product, cart and checkout', 'ai-eshop-optimizer' ), 'on' => is_array( $trust ) && ! empty( $trust['enabled'] ) ),
array( 'id' => 'free_shipping', 'tab' => 'free_shipping_display', 'kind' => 'storefront', 'what' => __( 'The free-shipping progress bar', 'ai-eshop-optimizer' ), 'on' => ( 'off' !== (string) $s( 'recipe.no_free_shipping_gate', 'hard' ) ) ),
array( 'id' => 'product_card', 'tab' => 'product_card', 'kind' => 'storefront', 'what' => __( 'Rooster One draws the product cards on archive pages', 'ai-eshop-optimizer' ), 'on' => ( 'aieo' === (string) get_option( 'aieo_dmm_archive_cards_design_source', '' ) ) ),
array( 'id' => 'gallery', 'tab' => 'gallery_layout', 'kind' => 'storefront', 'what' => __( 'Rooster One draws the product gallery or lightbox', 'ai-eshop-optimizer' ), 'on' => ( 'aieo' === (string) get_option( 'aieo_dmm_gallery_design_source', '' ) || 'aieo' === (string) get_option( 'aieo_dmm_lightbox_design_source', '' ) ) ),
array( 'id' => 'sticky_atc', 'tab' => 'sticky_atc', 'kind' => 'storefront', 'what' => __( 'The sticky add-to-cart bar on product pages', 'ai-eshop-optimizer' ), 'on' => (bool) $s( 'enabled_modules.sticky_atc', false ) ),
array( 'id' => 'wishlist', 'tab' => 'wishlist_display', 'kind' => 'storefront', 'what' => __( 'The wishlist heart on products', 'ai-eshop-optimizer' ), 'on' => $wish_on ),
array( 'id' => 'waitlist', 'tab' => 'waitlist_display', 'kind' => 'storefront', 'what' => __( 'The back-in-stock waitlist form', 'ai-eshop-optimizer' ), 'on' => is_array( $wait ) && ! empty( $wait['enabled'] ) ),
array( 'id' => 'consent', 'tab' => 'consent', 'kind' => 'storefront', 'what' => __( 'Rooster One\'s own cookie banner', 'ai-eshop-optimizer' ), 'on' => is_array( $consent ) && ( $consent['enabled'] ?? '' ) === 'yes' ),
array( 'id' => 'withdrawal', 'tab' => 'withdrawal', 'kind' => 'storefront', 'what' => __( 'The EU withdrawal-right button and forms', 'ai-eshop-optimizer' ), 'on' => class_exists( 'AIEO_Withdrawal' ) && method_exists( 'AIEO_Withdrawal', 'is_enabled' ) && (bool) AIEO_Withdrawal::is_enabled() ),
array( 'id' => 'session', 'tab' => 'session_analytics', 'kind' => 'capture', 'what' => __( 'Session tracking: which pages a visitor opens and what they add to the cart', 'ai-eshop-optimizer' ), 'on' => ( '1' === (string) get_option( 'aieo_session_tracking_enabled', '0' ) ) ),
array( 'id' => 'conversions', 'tab' => 'conversions', 'kind' => 'capture', 'what' => __( 'Pixels and server-side events to the ad platforms', 'ai-eshop-optimizer' ), 'on' => $conv_on ),
);
return (array) apply_filters( 'aieo_running_now_rows', $rows );
}
public static function stages() {
return array(
self::stage_a(),
self::stage_b(),
self::stage_c(),
self::stage_d(),
self::stage_e(),
self::stage_f(),
self::stage_g(),
);
}
private static function stage_f() {
$running = array();
foreach ( self::running_now() as $r ) {
$running[ (string) $r['id'] ] = $r;
}
$step = static function ( $id, $label, $running_id, $tab, $off_detail ) use ( $running ) {
$row = isset( $running[ $running_id ] ) ? $running[ $running_id ] : null;
return array(
'id' => $id,
'label' => $label,
'detect' => static function () use ( $row, $off_detail ) {
if ( null === $row ) {
return array( 'state' => self::STATE_NA, 'detail' => __( 'Not available in this build', 'ai-eshop-optimizer' ) );
}
return ! empty( $row['on'] )
? array( 'state' => self::STATE_SATISFIED, 'detail' => __( 'On', 'ai-eshop-optimizer' ) )
: array( 'state' => self::STATE_OPEN, 'detail' => $off_detail );
},
'action' => array( 'tab' => $tab, 'label' => __( 'Switch', 'ai-eshop-optimizer' ) ),
'dark' => '',
'na' => true,
'na_set' => 'storefront',
);
};
return array(
'id' => 'f',
'label' => __( 'The storefront', 'ai-eshop-optimizer' ),
'question' => __( 'Each of these replaces something your theme already draws. None is required; a shop with a theme it likes can set the whole stage aside in one click, and bring any single step back later.', 'ai-eshop-optimizer' ),
'na_sets' => array(
'storefront' => array(
'label' => __( 'This shop has a theme it likes: none of these apply', 'ai-eshop-optimizer' ),
'undo' => __( 'Undo: bring the storefront steps back', 'ai-eshop-optimizer' ),
),
),
'steps' => array(
$step( 'f_swatches', __( 'Colour and size swatches', 'ai-eshop-optimizer' ), 'swatches', 'swatches', __( 'Off. Variation dropdowns come from the theme.', 'ai-eshop-optimizer' ) ),
$step( 'f_product_card', __( 'Product cards on archive pages', 'ai-eshop-optimizer' ), 'product_card', 'product_card', __( 'Off. The theme draws the cards.', 'ai-eshop-optimizer' ) ),
$step( 'f_badges', __( 'Product badges: New, Sale, Low stock', 'ai-eshop-optimizer' ), 'badges', 'badges', __( 'Off. No badge is enabled.', 'ai-eshop-optimizer' ) ),
$step( 'f_trust_badges', __( 'Trust badges on product, cart and checkout', 'ai-eshop-optimizer' ), 'trust_badges', 'trust_badges', __( 'Off.', 'ai-eshop-optimizer' ) ),
$step( 'f_gallery', __( 'Product gallery and lightbox', 'ai-eshop-optimizer' ), 'gallery', 'gallery_layout', __( 'Off. The theme draws the gallery.', 'ai-eshop-optimizer' ) ),
$step( 'f_side_cart', __( 'The side cart', 'ai-eshop-optimizer' ), 'side_cart', 'side_cart', __( 'Off. Add to cart goes to the cart page.', 'ai-eshop-optimizer' ) ),
$step( 'f_sticky_atc', __( 'Sticky add-to-cart bar', 'ai-eshop-optimizer' ), 'sticky_atc', 'sticky_atc', __( 'Off.', 'ai-eshop-optimizer' ) ),
),
);
}
private static function stage_g() {
return array(
'id' => 'g',
'label' => __( 'Reach people, and the law', 'ai-eshop-optimizer' ),
'question' => __( 'Three ways to bring people back, which a shop may decline as one decision, and two obligations, which it may not. The withdrawal button is the only step in this checklist that is allowed to look urgent.', 'ai-eshop-optimizer' ),
'na_sets' => array(
'messaging' => array(
'label' => __( 'This shop does not message its customers: none of the three apply', 'ai-eshop-optimizer' ),
'undo' => __( 'Undo: it does message its customers', 'ai-eshop-optimizer' ),
),
),
'steps' => array(
array(
'id' => 'g_recovery',
'label' => __( 'Abandoned-cart recovery', 'ai-eshop-optimizer' ),
'detect' => static function () {
if ( ! class_exists( 'AIEO_Workflows' ) || ! method_exists( 'AIEO_Workflows', 'list_all' ) ) {
return array( 'state' => self::STATE_OPEN, 'detail' => __( 'Workflows are not available in this build', 'ai-eshop-optimizer' ) );
}
$active = 0;
$other = 0;
foreach ( (array) AIEO_Workflows::list_all() as $wf ) {
if ( ! in_array( (string) ( $wf['trigger_type'] ?? '' ), array( 'abandoned_cart', 'abandoned_checkout' ), true ) ) {
continue;
}
if ( 'active' === (string) ( $wf['status'] ?? '' ) ) {
$active++;
} else {
$other++;
}
}
if ( $active > 0 ) {
return array( 'state' => self::STATE_SATISFIED, 'detail' => sprintf( _n( '%d recovery workflow active', '%d recovery workflows active', $active, 'ai-eshop-optimizer' ), $active ) );
}
if ( $other > 0 ) {
return array( 'state' => self::STATE_PARTIAL, 'detail' => sprintf( _n( '%d recovery workflow exists but is not active', '%d recovery workflows exist but none is active', $other, 'ai-eshop-optimizer' ), $other ) );
}
return array( 'state' => self::STATE_OPEN, 'detail' => __( 'No recovery workflow yet', 'ai-eshop-optimizer' ) );
},
'action' => array( 'tab' => 'workflows', 'label' => __( 'Workflows', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Carts that were left stay left; the recovery report has nothing to report', 'ai-eshop-optimizer' ),
'na' => true,
'na_set' => 'messaging',
),
array(
'id' => 'g_push',
'label' => __( 'Web push: the keys, and someone to send to', 'ai-eshop-optimizer' ),
'detect' => static function () {
global $wpdb;
$vapid = get_option( 'aieo_push_vapid', array() );
if ( ! is_array( $vapid ) || empty( $vapid['public_b64url'] ) ) {
return array( 'state' => self::STATE_OPEN, 'detail' => __( 'No VAPID keys yet; nothing can be sent', 'ai-eshop-optimizer' ) );
}
$t = $wpdb->prefix . 'aieo_push_subscriptions';
$n = 0;
if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) === $t ) {
$n = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$t}`" );
}
$who = sprintf( _n( '%s subscriber', '%s subscribers', $n, 'ai-eshop-optimizer' ), number_format_i18n( $n ) );
if ( 'paused' === (string) get_option( 'aieo_push_engine_status', '' ) ) {
return array( 'state' => self::STATE_PARTIAL, 'detail' => sprintf( __( 'Keys in place, %s, sending paused', 'ai-eshop-optimizer' ), $who ) );
}
return array( 'state' => self::STATE_SATISFIED, 'detail' => sprintf( __( 'Keys in place, %s', 'ai-eshop-optimizer' ), $who ) );
},
'action' => array( 'tab' => 'web-push', 'label' => __( 'Web push', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Back-in-stock and price-drop alerts have no channel', 'ai-eshop-optimizer' ),
'na' => true,
'na_set' => 'messaging',
),
array(
'id' => 'g_newsletter',
'label' => __( 'A newsletter platform to send audiences to', 'ai-eshop-optimizer' ),
'detect' => static function () {
$o = get_option( 'aieo_newsletter_settings', array() );
if ( ! is_array( $o ) || empty( $o['provider'] ) ) {
return array( 'state' => self::STATE_OPEN, 'detail' => __( 'None connected', 'ai-eshop-optimizer' ) );
}
$p = (string) $o['provider'];
$cfg = ( isset( $o[ $p ] ) && is_array( $o[ $p ] ) ) ? array_filter( $o[ $p ] ) : array();
return $cfg
? array( 'state' => self::STATE_SATISFIED, 'detail' => strtoupper( $p ) )
: array( 'state' => self::STATE_PARTIAL, 'detail' => sprintf( __( '%s chosen, no credentials yet', 'ai-eshop-optimizer' ), strtoupper( $p ) ) );
},
'action' => array( 'tab' => 'web-push', 'label' => __( 'Newsletter platform', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Audiences are built but cannot be sent anywhere', 'ai-eshop-optimizer' ),
'na' => true,
'na_set' => 'messaging',
),
array(
'id' => 'g_consent',
'label' => __( 'A cookie banner that records consent', 'ai-eshop-optimizer' ),
'detect' => static function () {
$cmp = ( class_exists( 'AIEO_Consent' ) && method_exists( 'AIEO_Consent', 'external_cmp_active' ) ) ? (string) AIEO_Consent::external_cmp_active() : '';
$own = get_option( 'aieo_consent_settings', array() );
$own = is_array( $own ) && ( $own['enabled'] ?? '' ) === 'yes';
if ( '' !== $cmp ) {
return array( 'state' => self::STATE_SATISFIED, 'detail' => sprintf( __( '%s is active and Rooster One reads its consent', 'ai-eshop-optimizer' ), $cmp ) );
}
if ( $own ) {
return array( 'state' => self::STATE_SATISFIED, 'detail' => __( 'Rooster One\'s own banner is on', 'ai-eshop-optimizer' ) );
}
return array( 'state' => self::STATE_OPEN, 'detail' => __( 'No banner detected', 'ai-eshop-optimizer' ) );
},
'action' => array( 'tab' => 'consent', 'label' => __( 'Consent', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Nothing that needs consent may run, and the record a regulator asks for does not exist', 'ai-eshop-optimizer' ),
),
array(
'id' => 'g_withdrawal',
'label' => __( 'The EU withdrawal button (Directive 2023/2673)', 'ai-eshop-optimizer' ),
'detect' => static function () {
if ( ! class_exists( 'WooCommerce' ) ) {
return array( 'state' => self::STATE_NA, 'detail' => __( 'WooCommerce is not installed', 'ai-eshop-optimizer' ) );
}
$c = self::base_country();
if ( ! $c['eu'] ) {
return array( 'state' => self::STATE_NA, 'detail' => sprintf( __( 'Applies to shops based in the EU; this one is based in %s', 'ai-eshop-optimizer' ), $c['name'] ) );
}
$on = class_exists( 'AIEO_Withdrawal' ) && method_exists( 'AIEO_Withdrawal', 'is_enabled' ) && (bool) AIEO_Withdrawal::is_enabled();
if ( $on ) {
return array( 'state' => self::STATE_SATISFIED, 'detail' => __( 'The button, the form and the queue are on', 'ai-eshop-optimizer' ) );
}
return array( 'state' => self::STATE_OPEN, 'detail' => sprintf( __( 'A legal obligation, not a preference: this shop is based in %s and the button is off', 'ai-eshop-optimizer' ), $c['name'] ) );
},
'action' => array( 'tab' => 'withdrawal', 'label' => __( 'Withdrawal', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Consumers have no button to withdraw with, which the directive requires', 'ai-eshop-optimizer' ),
'urgent' => true,
),
),
);
}
private static function stage_b() {
return array(
'id' => 'b',
'label' => __( 'Bring your data in', 'ai-eshop-optimizer' ),
'question' => __( 'What the reports read. A stalled nightly chain is invisible until a report is wrong, and it has stalled for weeks before; the third step here is the one that earns its place.', 'ai-eshop-optimizer' ),
'steps' => array(
array(
'id' => 'b_erp',
'label' => __( 'Connect an ERP or accounting system, or say this is a catalogue-only shop', 'ai-eshop-optimizer' ),
'detect' => static function () {
$engine = class_exists( 'AIEO_Docscan_Engine' );
$active = class_exists( 'AIEO_DMM_ERP_Connectors' ) && class_exists( 'AIEO_DMM' ) && is_object( AIEO_DMM::instance()->get_module( 'erp_connectors' ) );
if ( ! $engine ) {
return array( 'state' => self::STATE_OPEN, 'detail' => __( 'The connectors ship in the Rooster One Pro plugin, which is not installed', 'ai-eshop-optimizer' ) );
}
$n = 0;
foreach ( array( 'aieo_erp_connectors', 'aieo_erp_connections', 'aieo_erp_bridge' ) as $opt ) {
$o = get_option( $opt, array() );
if ( is_array( $o ) ) {
foreach ( $o as $row ) {
if ( is_array( $row ) && array_filter( $row ) ) { $n++; }
}
}
}
return $n > 0
? array( 'state' => self::STATE_SATISFIED, 'detail' => sprintf( _n( '%d connector configured', '%d connectors configured', $n, 'ai-eshop-optimizer' ), $n ) )
: array( 'state' => self::STATE_OPEN, 'detail' => $active ? __( 'Engine present, nothing connected yet', 'ai-eshop-optimizer' ) : __( 'Engine present', 'ai-eshop-optimizer' ) );
},
'action' => array( 'tab' => 'erp_connectors', 'label' => __( 'Accounting & ERP', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Branch reports, purchases by supplier, landed cost and VAT reconciliation', 'ai-eshop-optimizer' ),
'na' => true,
),
array(
'id' => 'b_sources',
'label' => __( 'A catalogue source: a feed, a marketplace API, or a stock file', 'ai-eshop-optimizer' ),
'detect' => static function () {
$have = array();
foreach ( array( 'efood' => 'efood', 'etsy' => 'Etsy', 'wolt' => 'Wolt', 'amazon' => 'Amazon' ) as $slug => $label ) {
$c = get_option( 'aieo_catalog_import_' . $slug, array() );
if ( is_array( $c ) && ( ! empty( $c['client_id'] ) || ! empty( $c['vendor_id'] ) || ! empty( $c['api_key'] ) ) ) {
$have[] = $label;
}
}
global $wpdb;
$jobs = $wpdb->prefix . 'aieo_dmm_export_jobs';
$imports = 0;
if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $jobs ) ) === $jobs ) {
$imports = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$jobs}" );
}
if ( $have ) {
return array( 'state' => self::STATE_SATISFIED, 'detail' => sprintf( __( 'Connected: %s', 'ai-eshop-optimizer' ), implode( ', ', $have ) ) );
}
if ( $imports > 0 ) {
return array( 'state' => self::STATE_PARTIAL, 'detail' => sprintf( __( 'No live source, but %s file jobs on record', 'ai-eshop-optimizer' ), number_format_i18n( $imports ) ) );
}
return array( 'state' => self::STATE_OPEN, 'detail' => __( 'None yet', 'ai-eshop-optimizer' ) );
},
'action' => array( 'tab' => 'stock_import', 'label' => __( 'Stock and price import', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Nothing goes dark; the catalogue is simply maintained by hand', 'ai-eshop-optimizer' ),
'na' => true,
),
array(
'id' => 'b_nightly',
'label' => __( 'The nightly chain is running and current', 'ai-eshop-optimizer' ),
'detect' => static function () {
$all = get_option( 'aieo_analytics_jobs_last_run', array() );
$latest = 0;
$failed = array();
$jobs = 0;
if ( is_array( $all ) ) {
foreach ( $all as $job => $r ) {
if ( ! is_array( $r ) || empty( $r['finished_at'] ) ) { continue; }
$jobs++;
$ts = strtotime( (string) $r['finished_at'] . ' UTC' );
if ( $ts > $latest ) { $latest = $ts; }
if ( isset( $r['ok'] ) && ! $r['ok'] ) { $failed[] = (string) $job; }
}
}
if ( 0 === $latest ) {
return array( 'state' => self::STATE_OPEN, 'detail' => __( 'Has never run', 'ai-eshop-optimizer' ) );
}
$age = time() - $latest;
$when = sprintf( __( 'last finished %s ago', 'ai-eshop-optimizer' ), human_time_diff( $latest, time() ) );
if ( $failed ) {
return array( 'state' => self::STATE_PARTIAL, 'detail' => sprintf( __( '%1$s · failed: %2$s', 'ai-eshop-optimizer' ), $when, implode( ', ', array_slice( $failed, 0, 4 ) ) ) );
}
if ( $age > 36 * HOUR_IN_SECONDS ) {
return array( 'state' => self::STATE_PARTIAL, 'detail' => sprintf( __( 'Stalled: %s', 'ai-eshop-optimizer' ), $when ) );
}
return array( 'state' => self::STATE_SATISFIED, 'detail' => sprintf( __( '%1$d jobs, %2$s', 'ai-eshop-optimizer' ), $jobs, $when ) );
},
'action' => array( 'tab' => 'data_prep_scheduler', 'label' => __( 'Scheduled tasks', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Every Insights and ROI report goes stale silently', 'ai-eshop-optimizer' ),
),
array(
'id' => 'b_backfill',
'label' => __( 'The first backfill has completed', 'ai-eshop-optimizer' ),
'detect' => static function () {
global $wpdb;
$t = $wpdb->prefix . 'aieo_order_stats';
if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) !== $t ) {
return array( 'state' => self::STATE_OPEN, 'detail' => __( 'No order statistics table yet', 'ai-eshop-optimizer' ) );
}
$n = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$t}" );
return $n > 0
? array( 'state' => self::STATE_SATISFIED, 'detail' => sprintf( __( '%s orders in the statistics table', 'ai-eshop-optimizer' ), number_format_i18n( $n ) ) )
: array( 'state' => self::STATE_OPEN, 'detail' => __( 'Empty: waiting for the first run, or for the first orders', 'ai-eshop-optimizer' ) );
},
'action' => array( 'tab' => 'data_prep_scheduler', 'label' => __( 'Run the chain now', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Product and brand performance, attribution, audiences', 'ai-eshop-optimizer' ),
),
),
);
}
private static function stage_d() {
return array(
'id' => 'd',
'label' => __( 'Measure what happens', 'ai-eshop-optimizer' ),
'question' => __( 'Consent first, then the pixels and the server-side events, then what counts as a conversion. The last step is the only one that distinguishes configured from working.', 'ai-eshop-optimizer' ),
'steps' => array(
array(
'id' => 'd_consent',
'label' => __( 'A consent banner governs the pixels', 'ai-eshop-optimizer' ),
'detect' => static function () {
$cmp = ( class_exists( 'AIEO_Consent' ) && method_exists( 'AIEO_Consent', 'external_cmp_active' ) ) ? (string) AIEO_Consent::external_cmp_active() : '';
$own = get_option( 'aieo_consent_settings', array() );
$own = is_array( $own ) && ( $own['enabled'] ?? '' ) === 'yes';
if ( '' !== $cmp ) {
return array( 'state' => self::STATE_SATISFIED, 'detail' => sprintf( __( '%s is active and Rooster One reads its consent', 'ai-eshop-optimizer' ), $cmp ) );
}
if ( $own ) {
return array( 'state' => self::STATE_SATISFIED, 'detail' => __( 'Rooster One\'s own banner is on', 'ai-eshop-optimizer' ) );
}
return array( 'state' => self::STATE_OPEN, 'detail' => __( 'No banner detected; nothing fires until one records consent', 'ai-eshop-optimizer' ) );
},
'action' => array( 'tab' => 'consent', 'label' => __( 'Consent', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Pixels, analytics and server-side events all wait for consent', 'ai-eshop-optimizer' ),
),
array(
'id' => 'd_pixels',
'label' => __( 'GA4, Google Ads, Meta and TikTok pixels', 'ai-eshop-optimizer' ),
'detect' => static function () {
$o = get_option( 'aieo_dmm_conversions', array() );
$on = array();
if ( is_array( $o ) ) {
$map = array( 'ga_measurement_id' => 'GA4', 'gtm_container_id' => 'GTM', 'ads_conversion_id' => 'Google Ads', 'fb_pixel_id' => 'Meta', 'tiktok_pixel_id' => 'TikTok' );
foreach ( $map as $k => $l ) {
if ( ! empty( $o[ $k ] ) ) { $on[] = $l; }
}
}
return $on
? array( 'state' => self::STATE_SATISFIED, 'detail' => implode( ', ', $on ) )
: array( 'state' => self::STATE_OPEN, 'detail' => __( 'None configured', 'ai-eshop-optimizer' ) );
},
'action' => array( 'tab' => 'conversions', 'label' => __( 'Conversions', 'ai-eshop-optimizer' ) ),
'dark' => __( 'The ad platforms learn nothing from your sales', 'ai-eshop-optimizer' ),
),
array(
'id' => 'd_server_side',
'label' => __( 'Server-side events: Meta CAPI and Google Ads uploads', 'ai-eshop-optimizer' ),
'detect' => static function () {
$o = get_option( 'aieo_dmm_conversions', array() );
$on = array();
if ( is_array( $o ) ) {
if ( ! empty( $o['fb_access_token'] ) ) { $on[] = 'Meta CAPI'; }
if ( ! empty( $o['ads_oauth_refresh_token'] ) && ! empty( $o['ads_conversion_action_id'] ) ) { $on[] = 'Google Ads'; }
}
return $on
? array( 'state' => self::STATE_SATISFIED, 'detail' => implode( ', ', $on ) )
: array( 'state' => self::STATE_OPEN, 'detail' => __( 'Browser pixels only', 'ai-eshop-optimizer' ) );
},
'action' => array( 'tab' => 'conversions', 'label' => __( 'Conversions', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Ad-blocked and consent-limited sales never reach the platforms', 'ai-eshop-optimizer' ),
'na' => true,
),
array(
'id' => 'd_goals',
'label' => __( 'What counts as a conversion: forms and actions', 'ai-eshop-optimizer' ),
'detect' => static function () {
if ( ! class_exists( 'AIEO_Goal_Tracker' ) || ! method_exists( 'AIEO_Goal_Tracker', 'config' ) ) {
return array( 'state' => self::STATE_NA, 'detail' => __( 'Goal tracking is not available in this build', 'ai-eshop-optimizer' ) );
}
$cfg = AIEO_Goal_Tracker::config();
$forms = 0;
foreach ( (array) ( $cfg['forms'] ?? array() ) as $row ) {
if ( ( is_array( $row ) && ! empty( $row['on'] ) ) || ( ! is_array( $row ) && ! empty( $row ) ) ) { $forms++; }
}
$builtin = count( array_filter( (array) ( $cfg['builtin'] ?? array() ) ) );
$detail = sprintf( __( '%1$d built-in goals on, %2$d forms', 'ai-eshop-optimizer' ), $builtin, $forms );
return array( 'state' => ( $builtin + $forms ) > 0 ? self::STATE_SATISFIED : self::STATE_OPEN, 'detail' => $detail );
},
'action' => array( 'tab' => 'conversion_goals', 'label' => __( 'Conversion goals', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Campaign and landing-page reports count only purchases', 'ai-eshop-optimizer' ),
),
array(
'id' => 'd_events',
'label' => __( 'Something was actually captured in the last 7 days', 'ai-eshop-optimizer' ),
'detect' => static function () {
global $wpdb;
$t = $wpdb->prefix . 'aieo_sessions';
if ( '1' !== (string) get_option( 'aieo_session_tracking_enabled', '0' ) ) {
return array( 'state' => self::STATE_OPEN, 'detail' => __( 'Session tracking is off', 'ai-eshop-optimizer' ) );
}
if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) !== $t ) {
return array( 'state' => self::STATE_OPEN, 'detail' => __( 'No sessions table yet', 'ai-eshop-optimizer' ) );
}
$since = gmdate( 'Y-m-d H:i:s', time() - 8 * DAY_IN_SECONDS );
$n = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$t} WHERE created_at >= %s", $since ) );
return $n > 0
? array( 'state' => self::STATE_SATISFIED, 'detail' => sprintf( __( '%s sessions in the last 7 days', 'ai-eshop-optimizer' ), number_format_i18n( $n ) ) )
: array( 'state' => self::STATE_OPEN, 'detail' => __( 'Tracking is on but nothing has been captured for 7 days: check consent and the storefront', 'ai-eshop-optimizer' ) );
},
'action' => array( 'tab' => 'session_analytics', 'label' => __( 'Session analytics', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Every behaviour-based report and audience', 'ai-eshop-optimizer' ),
),
),
);
}
private static function stage_a() {
return array(
'id' => 'a',
'label' => __( 'Make it yours', 'ai-eshop-optimizer' ),
'question' => __( 'The identity every other surface reads: the organisation schema, the invoice header, the transactional email header and the Open Graph defaults are configured in four different screens and nothing tells you they are the same four values.', 'ai-eshop-optimizer' ),
'steps' => array(
array(
'id' => 'a_identity',
'label' => __( 'Business name and logo, and whether you are a company or a person', 'ai-eshop-optimizer' ),
'detect' => static function () {
$name = self::setting( 'seo.org_name', '' );
$logo = self::setting( 'seo.org_logo', '' );
$bits = array();
$bits[] = '' !== $name
? sprintf( __( 'Name: %s', 'ai-eshop-optimizer' ), $name )
: __( 'Name: not set (the site title is used)', 'ai-eshop-optimizer' );
$bits[] = '' !== $logo ? __( 'Logo: set', 'ai-eshop-optimizer' ) : __( 'Logo: missing', 'ai-eshop-optimizer' );
$state = ( '' !== $name && '' !== $logo ) ? self::STATE_SATISFIED : ( ( '' !== $name || '' !== $logo ) ? self::STATE_PARTIAL : self::STATE_OPEN );
return array( 'state' => $state, 'detail' => implode( ' · ', $bits ) );
},
'action' => array( 'tab' => 'seo_schema', 'anchor' => 'schema-organization', 'label' => __( 'Set name and logo', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Organisation schema, share cards, the JSON-LD block on every page', 'ai-eshop-optimizer' ),
),
array(
'id' => 'a_contact',
'label' => __( 'Contact details and address', 'ai-eshop-optimizer' ),
'detect' => static function () {
$addr = trim( (string) get_option( 'woocommerce_store_address', '' ) );
$city = trim( (string) get_option( 'woocommerce_store_city', '' ) );
$phone = trim( (string) self::setting( 'seo.org_phone', '' ) );
$have = ( '' !== $addr ? 1 : 0 ) + ( '' !== $city ? 1 : 0 ) + ( '' !== $phone ? 1 : 0 );
$bits = array();
$bits[] = ( '' !== $addr && '' !== $city )
? __( 'Store address: set', 'ai-eshop-optimizer' )
: __( 'Store address: incomplete in WooCommerce', 'ai-eshop-optimizer' );
$bits[] = '' !== $phone ? __( 'Phone: set', 'ai-eshop-optimizer' ) : __( 'Phone: missing', 'ai-eshop-optimizer' );
$state = 3 === $have ? self::STATE_SATISFIED : ( $have > 0 ? self::STATE_PARTIAL : self::STATE_OPEN );
return array( 'state' => $state, 'detail' => implode( ' · ', $bits ) );
},
'action' => array( 'url' => admin_url( 'admin.php?page=wc-settings&tab=general' ), 'label' => __( 'WooCommerce store address', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Local SEO, the invoice header, the contact block in schema', 'ai-eshop-optimizer' ),
),
array(
'id' => 'a_locale',
'label' => __( 'Country, currency and language', 'ai-eshop-optimizer' ),
'detect' => static function () {
if ( ! class_exists( 'WooCommerce' ) ) {
return array( 'state' => self::STATE_NA, 'detail' => __( 'WooCommerce is not installed', 'ai-eshop-optimizer' ) );
}
$raw = (string) get_option( 'woocommerce_default_country', '' );
$curr = function_exists( 'get_woocommerce_currency' ) ? (string) get_woocommerce_currency() : '';
$untouched = ( '' === $raw || 'US:CA' === $raw ) && ( '' === $curr || 'USD' === $curr );
$locale = (string) get_locale();
$has_mo = ( 'en_US' === $locale ) || ( defined( 'AIEO_DIR' ) && file_exists( AIEO_DIR . 'languages/ai-eshop-optimizer-' . $locale . '.mo' ) );
$bits = array();
$bits[] = $untouched
? __( 'WooCommerce still has its default country and currency (US, USD)', 'ai-eshop-optimizer' )
: sprintf( __( 'Country %1$s · currency %2$s', 'ai-eshop-optimizer' ), explode( ':', $raw )[0], $curr );
if ( ! $has_mo ) {
$bits[] = sprintf( __( 'No Rooster One translation for %s yet; the admin shows English', 'ai-eshop-optimizer' ), $locale );
}
if ( function_exists( 'wc_tax_enabled' ) && ! wc_tax_enabled() ) {
$bits[] = __( 'Taxes are off in WooCommerce', 'ai-eshop-optimizer' );
}
$state = $untouched ? self::STATE_OPEN : ( $has_mo ? self::STATE_SATISFIED : self::STATE_PARTIAL );
return array( 'state' => $state, 'detail' => implode( ' · ', $bits ) );
},
'action' => array( 'url' => admin_url( 'admin.php?page=wc-settings&tab=general' ), 'label' => __( 'WooCommerce general settings', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Every price, every date and the withdrawal-right texts', 'ai-eshop-optimizer' ),
),
array(
'id' => 'a_social',
'label' => __( 'Social profiles, for the share cards and the sameAs links', 'ai-eshop-optimizer' ),
'detect' => static function () {
$urls = self::setting( 'seo.social_urls', array() );
$urls = is_array( $urls ) ? array_filter( array_map( 'trim', array_map( 'strval', $urls ) ) ) : array();
$n = count( $urls );
return array(
'state' => $n > 0 ? self::STATE_SATISFIED : self::STATE_OPEN,
'detail' => $n > 0 ? sprintf( _n( '%d profile', '%d profiles', $n, 'ai-eshop-optimizer' ), $n ) : __( 'None yet', 'ai-eshop-optimizer' ),
);
},
'action' => array( 'tab' => 'seo_social', 'anchor' => 'social-overview', 'label' => __( 'Add social profiles', 'ai-eshop-optimizer' ) ),
'dark' => __( 'The sameAs block in the organisation schema', 'ai-eshop-optimizer' ),
'na' => true,
),
),
);
}
private static function stage_c() {
return array(
'id' => 'c',
'label' => __( 'Take over the page head (SEO)', 'ai-eshop-optimizer' ),
'question' => __( 'Two plugins must never both write the page head. The engine step cannot be reached before parity has been run at least once: the cut-over is the single most destructive thing this plugin can do to a shop, and the parity check exists so it is not done blind.', 'ai-eshop-optimizer' ),
'steps' => array(
array(
'id' => 'c_import',
'label' => __( 'Import what the other SEO plugin holds', 'ai-eshop-optimizer' ),
'detect' => static function () {
$det = self::seo_sources_detected();
if ( empty( $det ) ) {
return array( 'state' => self::STATE_NA, 'detail' => __( 'No other SEO plugin has data on this site', 'ai-eshop-optimizer' ) );
}
$last = (array) get_option( 'aieo_seo_import_last', array() );
$bits = array();
$src_labels = array();
foreach ( $det as $d ) {
$n = (int) ( $d['counts']['posts'] ?? 0 ) + (int) ( $d['counts']['terms'] ?? 0 );
$bits[] = sprintf( __( '%1$s holds data for %2$s objects', 'ai-eshop-optimizer' ), $d['label'], number_format_i18n( $n ) );
$src_labels[ (string) $d['id'] ] = (string) $d['label'];
}
$done = ! empty( $last['finished_at'] ) || ! empty( $last['source'] );
$lead = '';
if ( $done ) {
$src = isset( $last['source'] ) ? (string) $last['source'] : '';
$name = isset( $src_labels[ $src ] ) ? $src_labels[ $src ] : $src;
$when = ! empty( $last['finished_at'] ) ? wp_date( get_option( 'date_format' ), (int) $last['finished_at'] ) : '';
$lead = ( '' !== $name && '' !== $when )
? sprintf( __( 'Imported from %1$s on %2$s. ', 'ai-eshop-optimizer' ), $name, $when )
: __( 'Imported. ', 'ai-eshop-optimizer' );
}
return array(
'state' => $done ? self::STATE_SATISFIED : self::STATE_OPEN,
'detail' => $lead . implode( ' · ', $bits ),
);
},
'action' => array( 'tab' => 'seo_import', 'anchor' => 'import-source', 'label' => __( 'Open Migrations', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Titles, descriptions and redirects would start from blank after the cut-over', 'ai-eshop-optimizer' ),
),
array(
'id' => 'c_parity',
'label' => __( 'Compare the two page heads on real pages', 'ai-eshop-optimizer' ),
'detect' => static function () {
$v = get_option( self::OPTION_VERIFY_LAST, array() );
if ( ! is_array( $v ) || empty( $v['at'] ) ) {
return array( 'state' => self::STATE_OPEN, 'detail' => __( 'Not yet run', 'ai-eshop-optimizer' ) );
}
$mis = (int) ( $v['mismatches'] ?? 0 );
$detail = sprintf( __( 'Checked %1$d pages, %2$d mismatches, on %3$s', 'ai-eshop-optimizer' ), (int) ( $v['checked'] ?? 0 ), $mis, wp_date( get_option( 'date_format' ), (int) $v['at'] ) );
return array( 'state' => $mis > 0 ? self::STATE_PARTIAL : self::STATE_SATISFIED, 'detail' => $detail );
},
'action' => array( 'tab' => 'seo_import', 'anchor' => 'import-pre-cutover-verify-head-parity', 'label' => __( 'Run head parity', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Nothing goes dark; this is the check that stops the next step being blind', 'ai-eshop-optimizer' ),
),
array(
'id' => 'c_engine',
'label' => __( 'Turn on the Rooster One engine', 'ai-eshop-optimizer' ),
'detect' => static function () {
$on = (bool) self::setting( 'seo.enabled', false );
if ( $on ) {
return array( 'state' => self::STATE_SATISFIED, 'detail' => __( 'Enabled', 'ai-eshop-optimizer' ) );
}
$v = get_option( self::OPTION_VERIFY_LAST, array() );
$ran = is_array( $v ) && ! empty( $v['at'] );
return array(
'state' => self::STATE_OPEN,
'detail' => $ran ? __( 'Off', 'ai-eshop-optimizer' ) : __( 'Off. Run head parity first; the switch is on SEO → General once you have.', 'ai-eshop-optimizer' ),
);
},
'action' => array( 'tab' => 'seo_general', 'anchor' => 'general-seo-engine', 'label' => __( 'SEO → General', 'ai-eshop-optimizer' ) ),
'dark' => __( 'The whole SEO menu stays read-only', 'ai-eshop-optimizer' ),
),
array(
'id' => 'c_deactivate',
'label' => __( 'Deactivate the other SEO plugin. Last, and yours to do', 'ai-eshop-optimizer' ),
'detect' => static function () {
$c = self::competitor();
if ( '' === $c ) {
return array( 'state' => self::STATE_SATISFIED, 'detail' => __( 'No other SEO plugin is active', 'ai-eshop-optimizer' ) );
}
if ( class_exists( 'AIEO_Overlap' ) && method_exists( 'AIEO_Overlap', 'decision' ) && 'kept' === AIEO_Overlap::decision( 'seo' ) ) {
return array( 'state' => self::STATE_NA, 'detail' => sprintf( __( '%s owns the page head, by your choice', 'ai-eshop-optimizer' ), $c ) );
}
return array( 'state' => self::STATE_OPEN, 'detail' => sprintf( __( '%s is active and owns the page head', 'ai-eshop-optimizer' ), $c ) );
},
'action' => array( 'url' => admin_url( 'plugins.php' ), 'label' => __( 'Plugins', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Rooster One stands back from the head while it is active', 'ai-eshop-optimizer' ),
),
array(
'id' => 'c_sitemap',
'label' => __( 'Sitemap and robots served', 'ai-eshop-optimizer' ),
'detect' => static function () {
if ( ! class_exists( 'AIEO_SEO_Store' ) || ! AIEO_SEO_Store::is_active_provider() ) {
return array( 'state' => self::STATE_NA, 'detail' => __( 'Built once Rooster One owns the page head', 'ai-eshop-optimizer' ) );
}
$dir = ( class_exists( 'AIEO_SEO_Sitemap' ) && method_exists( 'AIEO_SEO_Sitemap', 'dir' ) ) ? (string) AIEO_SEO_Sitemap::dir() : '';
$newest = 0;
if ( '' !== $dir && is_dir( $dir ) ) {
foreach ( (array) glob( trailingslashit( $dir ) . '*.xml' ) as $f ) {
$newest = max( $newest, (int) @filemtime( $f ) );
}
}
if ( $newest > 0 ) {
return array( 'state' => self::STATE_SATISFIED, 'detail' => sprintf( __( 'Written %s ago', 'ai-eshop-optimizer' ), human_time_diff( $newest, time() ) ) );
}
return array( 'state' => self::STATE_OPEN, 'detail' => __( 'No sitemap file yet', 'ai-eshop-optimizer' ) );
},
'action' => array( 'tab' => 'seo_general', 'anchor' => 'general-head-cache', 'label' => __( 'Rebuild', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Search engines keep reading the old sitemap', 'ai-eshop-optimizer' ),
),
),
);
}
private static function stage_e() {
return array(
'id' => 'e',
'label' => __( 'Money in, money out', 'ai-eshop-optimizer' ),
'question' => __( 'Every empty ROI screen in the plugin traces to a blank row here. Each step names the screen that goes dark without it.', 'ai-eshop-optimizer' ),
'steps' => array(
array(
'id' => 'e_cogs',
'label' => __( 'Cost of goods on the catalogue', 'ai-eshop-optimizer' ),
'detect' => static function () {
$c = self::cogs_coverage();
if ( $c['products'] < 1 ) {
return array( 'state' => self::STATE_OPEN, 'detail' => __( 'Waiting for products', 'ai-eshop-optimizer' ) );
}
$detail = sprintf( __( 'Cost known for %1$s of %2$s products', 'ai-eshop-optimizer' ), number_format_i18n( $c['with_cost'] ), number_format_i18n( $c['products'] ) );
$share = $c['with_cost'] / max( 1, $c['products'] );
$state = 0 === $c['with_cost'] ? self::STATE_OPEN : ( $share >= self::COVERAGE_SATISFIED ? self::STATE_SATISFIED : self::STATE_PARTIAL );
return array( 'state' => $state, 'detail' => $detail );
},
'action' => array( 'tab' => 'brand_margins', 'label' => __( 'Apply margins to Cost of Goods', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Margin by brand, Costs & ROI, every profit column', 'ai-eshop-optimizer' ),
),
array(
'id' => 'e_brand_margins',
'label' => __( 'A margin on each brand', 'ai-eshop-optimizer' ),
'detect' => static function () {
global $wpdb;
$tax = taxonomy_exists( 'product_brand' ) ? 'product_brand' : ( taxonomy_exists( 'pwb-brand' ) ? 'pwb-brand' : '' );
if ( '' === $tax ) {
return array( 'state' => self::STATE_NA, 'detail' => __( 'No brand taxonomy on this site', 'ai-eshop-optimizer' ) );
}
$brands = (int) wp_count_terms( array( 'taxonomy' => $tax, 'hide_empty' => false ) );
if ( $brands < 1 ) {
return array( 'state' => self::STATE_OPEN, 'detail' => __( 'No brands yet', 'ai-eshop-optimizer' ) );
}
$table = $wpdb->prefix . 'aieo_dmm_brand_extras';
$with = 0;
if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table ) {
$with = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE gross_margin IS NOT NULL AND gross_margin > 0" );
}
$store = class_exists( 'AIEO_DMM' ) && method_exists( 'AIEO_DMM', 'get_gross_margin_pct' ) ? (float) AIEO_DMM::get_gross_margin_pct() : 0;
$detail = sprintf( __( '%1$s of %2$s brands have their own margin; the rest use the store default of %3$s%%', 'ai-eshop-optimizer' ), number_format_i18n( $with ), number_format_i18n( $brands ), number_format_i18n( $store ) );
$share = $with / max( 1, $brands );
$state = 0 === $with ? self::STATE_OPEN : ( $share >= self::COVERAGE_SATISFIED ? self::STATE_SATISFIED : self::STATE_PARTIAL );
return array( 'state' => $state, 'detail' => $detail );
},
'action' => array( 'tab' => 'brand_margins', 'label' => __( 'Brand margins', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Margin by brand reads one flat number for every brand', 'ai-eshop-optimizer' ),
'na' => true,
),
array(
'id' => 'e_shipping_rates',
'label' => __( 'What each courier charges you', 'ai-eshop-optimizer' ),
'detect' => static function () {
global $wpdb;
$table = $wpdb->prefix . 'aieo_dmm_shipping_rates';
if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
return array( 'state' => self::STATE_OPEN, 'detail' => __( 'No rate cards yet', 'ai-eshop-optimizer' ) );
}
$n = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
return array(
'state' => $n > 0 ? self::STATE_SATISFIED : self::STATE_OPEN,
'detail' => $n > 0 ? sprintf( __( '%s rate rows', 'ai-eshop-optimizer' ), number_format_i18n( $n ) ) : __( 'No rate cards yet', 'ai-eshop-optimizer' ),
);
},
'action' => array( 'tab' => 'shipping_rates', 'label' => __( 'Shipping rate cards', 'ai-eshop-optimizer' ) ),
'dark' => __( 'Shipping cost per order in Costs & ROI', 'ai-eshop-optimizer' ),
'na' => true,
),
array(
'id' => 'e_ad_connectors',
'label' => __( 'Where your ad spend comes from', 'ai-eshop-optimizer' ),
'detect' => static function () {
$o = get_option( 'aieo_ad_connectors', array() );
$on = array();
if ( is_array( $o ) ) {
foreach ( $o as $k => $v ) {
if ( is_array( $v ) && array_filter( $v ) ) {
$on[] = ucwords( str_replace( '_', ' ', (string) $k ) );
}
}
}
return array(
'state' => $on ? self::STATE_SATISFIED : self::STATE_OPEN,
'detail' => $on ? implode( ', ', $on ) : __( 'None connected', 'ai-eshop-optimizer' ),
);
},
'action' => array( 'tab' => 'ad_connectors', 'label' => __( 'Connect ad accounts', 'ai-eshop-optimizer' ) ),
'dark' => __( 'ROI shows revenue against zero spend', 'ai-eshop-optimizer' ),
'na' => true,
),
),
);
}
private static function setting( $key, $default ) {
if ( ! class_exists( 'AIEO_DMM_Settings' ) ) {
return $default;
}
$v = AIEO_DMM_Settings::get( $key, $default );
return is_string( $v ) ? trim( $v ) : $v;
}
private static function competitor() {
if ( ! class_exists( 'AIEO_SEO_Store' ) && defined( 'AIEO_DIR' ) && file_exists( AIEO_DIR . 'includes/seo/class-aieo-seo-store.php' ) ) {
require_once AIEO_DIR . 'includes/seo/class-aieo-seo-store.php';
}
return ( class_exists( 'AIEO_SEO_Store' ) && method_exists( 'AIEO_SEO_Store', 'competitor_seo_plugin' ) ) ? (string) AIEO_SEO_Store::competitor_seo_plugin() : '';
}
public static function seo_sources_detected() {
if ( ! class_exists( 'AIEO_SEO_Import_Registry' ) && defined( 'AIEO_DIR' ) && file_exists( AIEO_DIR . 'includes/seo/import/class-aieo-seo-import-registry.php' ) ) {
require_once AIEO_DIR . 'includes/seo/import/class-aieo-seo-import-registry.php';
}
if ( ! class_exists( 'AIEO_SEO_Import_Registry' ) ) {
return array();
}
return (array) AIEO_SEO_Import_Registry::detected();
}
private static function action_url( $action ) {
if ( ! is_array( $action ) ) {
return '';
}
if ( ! empty( $action['url'] ) ) {
return (string) $action['url'];
}
if ( empty( $action['tab'] ) || ! class_exists( 'AIEO_DMM' ) ) {
return '';
}
$url = AIEO_DMM::instance()->get_subtab_url( (string) $action['tab'] );
if ( ! empty( $action['anchor'] ) ) {
$url .= '#aieo-card-' . sanitize_title( (string) $action['anchor'] );
}
return $url;
}
private static function tier_for( $tab ) {
if ( '' === (string) $tab || ! class_exists( 'AIEO_Capability_Registry' ) || ! method_exists( 'AIEO_Capability_Registry', 'tier_for' ) ) {
return '';
}
$r = AIEO_Capability_Registry::tier_for( (string) $tab );
$t = is_array( $r ) && isset( $r['tier'] ) ? (string) $r['tier'] : (string) $r;
return in_array( $t, array( 'free', 'pro' ), true ) ? $t : '';
}
}