prefix . 'aieo_ads_plans'; } public static function ensure_table() { global $wpdb; $t = self::table(); if ( self::$table_ready ) { return $t; } $exists = ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) === $t ); if ( ! $exists ) { $charset = function_exists( 'aieo_get_charset_collate' ) ? aieo_get_charset_collate() : $wpdb->get_charset_collate(); $wpdb->query( "CREATE TABLE IF NOT EXISTS `{$t}` ( `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, `status` VARCHAR(16) NOT NULL DEFAULT 'draft', `objective` VARCHAR(16) NOT NULL DEFAULT 'sales', `platform` VARCHAR(16) NOT NULL DEFAULT 'google', `name` VARCHAR(191) NOT NULL DEFAULT '', `source_type` VARCHAR(32) NOT NULL DEFAULT '', `source_id` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, `daily_budget` DECIMAL(12,2) NOT NULL DEFAULT 0, `currency` VARCHAR(8) NOT NULL DEFAULT '', `plan_json` LONGTEXT NULL, `external_ids_json` LONGTEXT NULL, `external_campaign_id` VARCHAR(64) NOT NULL DEFAULT '', `last_error` TEXT NULL, `created_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `pushed_at` DATETIME NULL, `activated_at` DATETIME NULL, PRIMARY KEY (`id`), KEY `idx_status` (`status`), KEY `idx_source` (`source_type`,`source_id`), KEY `idx_external` (`external_campaign_id`) ) " . $charset ); } if ( function_exists( 'aieo_dmm_ensure_column' ) && (string) get_option( self::SCHEMA_OPT, '' ) !== self::SCHEMA_VERSION && AIEO_Schema_Lock::acquire( self::SCHEMA_OPT ) ) { aieo_dmm_ensure_column( $t, 'currency', "VARCHAR(8) NOT NULL DEFAULT ''" ); aieo_dmm_ensure_column( $t, 'activated_at', 'DATETIME NULL' ); update_option( self::SCHEMA_OPT, self::SCHEMA_VERSION, false ); } self::$table_ready = true; return $t; } public static function save_draft( array $plan ) { global $wpdb; $t = self::ensure_table(); $name = isset( $plan['name'] ) ? (string) $plan['name'] : ''; if ( '' === trim( $name ) ) { return new WP_Error( 'aieo_ads_no_name', __( 'A campaign plan needs a name.', 'ai-eshop-optimizer' ) ); } $budget = isset( $plan['daily_budget'] ) ? (float) $plan['daily_budget'] : 0.0; if ( $budget <= 0 ) { return new WP_Error( 'aieo_ads_no_budget', __( 'A campaign plan needs a daily budget above zero.', 'ai-eshop-optimizer' ) ); } if ( empty( $plan['ad_groups'] ) || ! is_array( $plan['ad_groups'] ) ) { return new WP_Error( 'aieo_ads_no_ad_groups', __( 'A campaign plan needs at least one ad group.', 'ai-eshop-optimizer' ) ); } $ok = $wpdb->insert( $t, array( 'status' => self::STATUS_DRAFT, 'objective' => self::valid_objective( isset( $plan['objective'] ) ? $plan['objective'] : '' ), 'platform' => 'google', 'name' => mb_substr( $name, 0, 191 ), 'source_type' => isset( $plan['source_type'] ) ? (string) $plan['source_type'] : '', 'source_id' => isset( $plan['source_id'] ) ? (int) $plan['source_id'] : 0, 'daily_budget' => $budget, 'currency' => isset( $plan['currency'] ) ? (string) $plan['currency'] : '', 'plan_json' => wp_json_encode( $plan ), 'created_by' => get_current_user_id(), 'created_at' => current_time( 'mysql', true ), ), array( '%s', '%s', '%s', '%s', '%s', '%d', '%f', '%s', '%s', '%d', '%s' ) ); if ( ! $ok ) { return new WP_Error( 'aieo_ads_insert_failed', __( 'Could not save the campaign plan.', 'ai-eshop-optimizer' ) ); } return (int) $wpdb->insert_id; } public static function get( $id ) { global $wpdb; $t = self::ensure_table(); $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$t}` WHERE id = %d", (int) $id ), ARRAY_A ); return $row ? self::hydrate( $row ) : null; } public static function all( $status = '', $limit = 50 ) { global $wpdb; $t = self::ensure_table(); $limit = max( 1, min( 500, (int) $limit ) ); if ( '' !== $status ) { $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$t}` WHERE status = %s ORDER BY id DESC LIMIT %d", $status, $limit ), ARRAY_A ); } else { $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$t}` WHERE status <> %s ORDER BY id DESC LIMIT %d", self::STATUS_ARCHIVED, $limit ), ARRAY_A ); } return array_map( array( __CLASS__, 'hydrate' ), (array) $rows ); } public static function mark_created( $id, array $external ) { global $wpdb; $t = self::ensure_table(); return (bool) $wpdb->update( $t, array( 'status' => self::STATUS_CREATED, 'external_ids_json' => wp_json_encode( $external ), 'external_campaign_id' => isset( $external['campaign_id'] ) ? (string) $external['campaign_id'] : '', 'pushed_at' => current_time( 'mysql', true ), 'last_error' => '', ), array( 'id' => (int) $id ), array( '%s', '%s', '%s', '%s', '%s' ), array( '%d' ) ); } public static function mark_activated( $id ) { global $wpdb; $t = self::ensure_table(); return (bool) $wpdb->update( $t, array( 'status' => self::STATUS_ACTIVATED, 'activated_at' => current_time( 'mysql', true ) ), array( 'id' => (int) $id ), array( '%s', '%s' ), array( '%d' ) ); } public static function mark_failed( $id, $error ) { global $wpdb; $t = self::ensure_table(); return (bool) $wpdb->update( $t, array( 'status' => self::STATUS_FAILED, 'last_error' => mb_substr( (string) $error, 0, 2000 ) ), array( 'id' => (int) $id ), array( '%s', '%s' ), array( '%d' ) ); } public static function archive( $id ) { global $wpdb; $t = self::ensure_table(); return (bool) $wpdb->update( $t, array( 'status' => self::STATUS_ARCHIVED ), array( 'id' => (int) $id ), array( '%s' ), array( '%d' ) ); } public static function valid_objective( $o ) { return ( self::OBJECTIVE_LEADS === (string) $o ) ? self::OBJECTIVE_LEADS : self::OBJECTIVE_SALES; } public static function status_label( $status ) { switch ( (string) $status ) { case self::STATUS_DRAFT: return __( 'Draft — nothing in Google yet', 'ai-eshop-optimizer' ); case self::STATUS_CREATED: return __( 'In Google, paused', 'ai-eshop-optimizer' ); case self::STATUS_ACTIVATED: return __( 'Live', 'ai-eshop-optimizer' ); case self::STATUS_FAILED: return __( 'Failed', 'ai-eshop-optimizer' ); case self::STATUS_ARCHIVED: return __( 'Archived', 'ai-eshop-optimizer' ); } return (string) $status; } private static function hydrate( array $row ) { $row['plan'] = json_decode( (string) ( isset( $row['plan_json'] ) ? $row['plan_json'] : '' ), true ); $row['external_ids'] = json_decode( (string) ( isset( $row['external_ids_json'] ) ? $row['external_ids_json'] : '' ), true ); if ( ! is_array( $row['plan'] ) ) { $row['plan'] = array(); } if ( ! is_array( $row['external_ids'] ) ) { $row['external_ids'] = array(); } return $row; } }