prefix . 'chargeforms_notification_log'; } /** @return int The new log row's id, used to update its status after the send attempt (and to re-look-up the row for a scheduled retry). */ public static function create( $form_id, $entry_id, $to_email, $subject ) { global $wpdb; $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- custom plugin table, no core API exists for it. self::table(), [ 'form_id' => (int) $form_id, 'entry_id' => $entry_id ? (int) $entry_id : null, 'to_email' => $to_email, 'subject' => $subject, 'status' => 'pending', 'attempts' => 0, // GMT, explicit (roadmap issue #27) — not left to the // table's own DEFAULT CURRENT_TIMESTAMP. 'created_at' => current_time( 'mysql', true ), ], [ '%d', '%d', '%s', '%s', '%s', '%d', '%s' ] ); return (int) $wpdb->insert_id; } public static function mark_sent( $id ) { global $wpdb; $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table, no core API exists for it; per-request freshness needed; not worth an object-cache layer for these tables. self::table(), [ 'status' => 'sent', 'sent_at' => current_time( 'mysql', true ) ], // GMT — matches created_at above. [ 'id' => (int) $id ], [ '%s', '%s' ], [ '%d' ] ); } /** `$will_retry` distinguishes "failed, but a retry is scheduled" from "failed, no more retries left" — both are failures right now, but only one still has hope, and the admin-visible status list (Tools > Notifications) should say which. */ public static function mark_failed( $id, $error_message, $will_retry ) { global $wpdb; $row = self::get( $id ); $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table, no core API exists for it; per-request freshness needed; not worth an object-cache layer for these tables. self::table(), [ 'status' => $will_retry ? 'retrying' : 'failed', 'attempts' => ( $row['attempts'] ?? 0 ) + 1, 'last_error' => substr( (string) $error_message, 0, 500 ), ], [ 'id' => (int) $id ], [ '%s', '%d', '%s' ], [ '%d' ] ); } public static function get( $id ) { global $wpdb; return $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', self::table(), $id ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table, no core API exists for it; per-request freshness needed; not worth an object-cache layer for these tables. } /** Paginated delivery history — Tools > Notifications' admin-visible status list (roadmap issue #20). Mirrors Api_Log::query()'s shape/pagination convention. */ public static function query( array $args = [] ) { global $wpdb; $page = max( 1, (int) ( $args['page'] ?? 1 ) ); $per_page = max( 1, (int) ( $args['per_page'] ?? 20 ) ); $offset = ( $page - 1 ) * $per_page; $table = self::table(); $total = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $table ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table, no core API exists for it; per-request freshness needed. $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table, no core API exists for it; per-request freshness needed; not worth an object-cache layer for these tables. $wpdb->prepare( 'SELECT * FROM %i ORDER BY created_at DESC, id DESC LIMIT %d OFFSET %d', $table, $per_page, $offset ), ARRAY_A ); return [ 'items' => $rows ?: [], 'total' => $total, 'page' => $page, 'per_page' => $per_page, ]; } /** * How many notifications have permanently failed (retries exhausted, * not just still-retrying) in the last N hours — the number an admin * notice needs to decide whether to warn at all. * * A site owner debugging "why didn't I get an email" today had to * already know to check Tools > Notifications and hover a status pill * to find this out for themselves. Counting failures here is what lets * an admin notice surface the same fact proactively, on the screens a * site owner is already looking at, instead of only on request. */ public static function count_recent_failures( $hours = 24 ) { global $wpdb; $table = self::table(); $since = gmdate( 'Y-m-d H:i:s', time() - ( max( 1, (int) $hours ) * HOUR_IN_SECONDS ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- small admin-notice check, not a hot path. return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE status = 'failed' AND created_at >= %s", $table, $since ) ); } }