prefix . 'schema_ai_usage' ); $today = current_time( 'Y-m-d' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; no WP API alternative exists. $existing = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. "SELECT id FROM {$table} WHERE user_id = %d AND date = %s", $user_id, $today ) ); if ( $existing ) { // Column name comes from ALLOWED_FIELDS whitelist above. // Each case is hardcoded to avoid any dynamic interpolation in SQL. if ( 'ai_generations' === $field ) { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. $sql = $wpdb->prepare( "UPDATE {$table} SET ai_generations = ai_generations + 1 WHERE id = %d", $existing ); } elseif ( 'manual_generations' === $field ) { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. $sql = $wpdb->prepare( "UPDATE {$table} SET manual_generations = manual_generations + 1 WHERE id = %d", $existing ); } else { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. $sql = $wpdb->prepare( "UPDATE {$table} SET validations = validations + 1 WHERE id = %d", $existing ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Required for custom table; write/DDL operation, caching not applicable. $result = $wpdb->query( $sql ); return false !== $result; } // Build the insert data with all fields defaulting to zero. $data = array( 'user_id' => $user_id, 'date' => $today, 'ai_generations' => 0, 'manual_generations' => 0, 'validations' => 0, ); // Set the requested field to 1. $data[ $field ] = 1; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; write operation (INSERT), caching not applicable. $result = $wpdb->insert( $table, $data, array( '%d', '%s', '%d', '%d', '%d' ) ); return false !== $result; } /** * Get today's usage row for a user. * * @param int $user_id WordPress user ID. * @return object|null Row object with ai_generations, manual_generations, * validations, date, and last_reset. Null if no row exists. */ public function get_daily( $user_id ) { global $wpdb; $table = esc_sql( $wpdb->prefix . 'schema_ai_usage' ); $today = current_time( 'Y-m-d' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; no WP API alternative exists. $row = $wpdb->get_row( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. "SELECT * FROM {$table} WHERE user_id = %d AND date = %s", $user_id, $today ) ); return $row; } /** * Get usage rows for a user within a date range (inclusive). * * @param int $user_id WordPress user ID. * @param string $start_date Start date in Y-m-d format. * @param string $end_date End date in Y-m-d format. * @return array Array of row objects. */ public function get_range( $user_id, $start_date, $end_date ) { global $wpdb; $table = esc_sql( $wpdb->prefix . 'schema_ai_usage' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; no WP API alternative exists. $results = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. "SELECT * FROM {$table} WHERE user_id = %d AND date >= %s AND date <= %s ORDER BY date ASC", $user_id, $start_date, $end_date ) ); return $results ? $results : array(); } /** * Get all-time aggregate totals for a user. * * @param int $user_id WordPress user ID. * @return object { * @type int $ai_generations Total AI generations. * @type int $manual_generations Total manual generations. * @type int $validations Total validations. * @type int $total_days Number of days with activity. * } */ public function get_totals( $user_id ) { global $wpdb; $table = esc_sql( $wpdb->prefix . 'schema_ai_usage' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; no WP API alternative exists. $row = $wpdb->get_row( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. "SELECT COALESCE( SUM( ai_generations ), 0 ) AS ai_generations, COALESCE( SUM( manual_generations ), 0 ) AS manual_generations, COALESCE( SUM( validations ), 0 ) AS validations, COUNT( * ) AS total_days FROM {$table} WHERE user_id = %d", $user_id ) ); if ( ! $row ) { return (object) array( 'ai_generations' => 0, 'manual_generations' => 0, 'validations' => 0, 'total_days' => 0, ); } return (object) array( 'ai_generations' => (int) $row->ai_generations, 'manual_generations' => (int) $row->manual_generations, 'validations' => (int) $row->validations, 'total_days' => (int) $row->total_days, ); } /** * Reset daily counters. * * Intended to be called by the schema_ai_usage_reset cron event. * It does not delete rows; instead it inserts fresh zero-count rows * for every user who had activity the previous day, ensuring a * clean slate. The actual reset is automatic because get_daily() * looks for today's date, and a new day means a new row. * * This method serves as a hook point for any additional reset logic * (e.g., firing notifications, syncing external services). */ public function reset_daily() { /** * Fires when the daily usage counters are reset. * * @since 1.0.0 */ do_action( 'schema_ai_usage_daily_reset' ); } /** * Delete usage records older than a specified number of days. * * @param int $days Number of days to keep. Records older than this are deleted. Default 30. * @return int Number of rows deleted. */ public function cleanup( $days = 30 ) { global $wpdb; $table = esc_sql( $wpdb->prefix . 'schema_ai_usage' ); $cutoff = gmdate( 'Y-m-d', strtotime( '-' . absint( $days ) . ' days' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; write/DDL operation, caching not applicable. $deleted = $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. "DELETE FROM {$table} WHERE date < %s", $cutoff ) ); return $deleted ? (int) $deleted : 0; } }