PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.0.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.0.0
1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
suredonation / inc / campaigns / campaign-stats.php

campaign-stats.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.0.0, at inc/campaigns/campaign-stats.php

325 lines 10.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Campaign Statistics Helper
4 *
5 * @package SureDonation
6 */
7
8 namespace SureDonation\Inc\Campaigns;
9
10 use SureDonation\Inc\Helper;
11
12 // Exit if accessed directly.
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Campaign_Stats class.
19 *
20 * @since 0.0.1
21 */
22 class Campaign_Stats {
23 /**
24 * Rows fetched into the cached recent-donations / top-donors transients.
25 *
26 * A fixed window (rather than the caller's limit) keeps one cache entry
27 * per campaign regardless of the block's display limit; callers slice
28 * down to what they need. Matches the largest fetch any block performs.
29 */
30 private const LIST_CACHE_SIZE = 100;
31
32 /**
33 * Get campaign statistics.
34 *
35 * @param int $campaign_id Campaign post ID.
36 * @return array<string, mixed> Campaign statistics.
37 * @since 0.0.1
38 */
39 public static function get_stats( $campaign_id ) {
40 global $wpdb;
41
42 $donations_table = $wpdb->prefix . 'suredonation_donations';
43
44 // Get total raised amount (completed and partially refunded donations, minus refunds).
45 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
46 $total_raised = $wpdb->get_var(
47 $wpdb->prepare(
48 "SELECT COALESCE(SUM(amount - refunded_amount), 0)
49 FROM %i
50 WHERE campaign_id = %d
51 AND payment_status IN ('completed', 'partially_refunded')",
52 $donations_table,
53 $campaign_id
54 )
55 );
56
57 // Get total donation count (completed and partially refunded).
58 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
59 $donation_count = $wpdb->get_var(
60 $wpdb->prepare(
61 "SELECT COUNT(*)
62 FROM %i
63 WHERE campaign_id = %d
64 AND payment_status IN ('completed', 'partially_refunded')",
65 $donations_table,
66 $campaign_id
67 )
68 );
69
70 // Get unique donor count (completed and partially refunded).
71 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
72 $donor_count = $wpdb->get_var(
73 $wpdb->prepare(
74 "SELECT COUNT(DISTINCT donor_email)
75 FROM %i
76 WHERE campaign_id = %d
77 AND payment_status IN ('completed', 'partially_refunded')
78 AND donor_email IS NOT NULL
79 AND donor_email != ''",
80 $donations_table,
81 $campaign_id
82 )
83 );
84
85 // Get goal type and amount from consolidated meta.
86 $campaign_meta = Helper::get_campaign_meta( $campaign_id );
87 $goal_type = $campaign_meta['goal_type'];
88 $goal_amount = floatval( Helper::get_string_value( $campaign_meta['goal_amount'] ) );
89
90 // Calculate progress percentage based on goal type.
91 $progress_percentage = 0;
92 if ( $goal_amount > 0 ) {
93 if ( 'donation_count' === $goal_type ) {
94 $progress_percentage = intval( $donation_count ) / $goal_amount * 100;
95 } else {
96 $progress_percentage = floatval( $total_raised ) / $goal_amount * 100;
97 }
98 $progress_percentage = min( $progress_percentage, 100 ); // Cap at 100%.
99 }
100
101 // Get average donation amount.
102 $average_donation = 0;
103 if ( $donation_count > 0 ) {
104 $average_donation = floatval( $total_raised ) / intval( $donation_count );
105 }
106
107 // Get largest single donation (net of refunds).
108 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
109 $largest_donation = $wpdb->get_var(
110 $wpdb->prepare(
111 "SELECT COALESCE(MAX(amount - refunded_amount), 0)
112 FROM %i
113 WHERE campaign_id = %d
114 AND payment_status IN ('completed', 'partially_refunded')",
115 $donations_table,
116 $campaign_id
117 )
118 );
119
120 // Get campaign status from consolidated meta.
121 $campaign_status = $campaign_meta['campaign_status'];
122
123 return [
124 'total_raised' => floatval( $total_raised ),
125 'goal_amount' => $goal_amount,
126 'donation_count' => intval( $donation_count ),
127 'donor_count' => intval( $donor_count ),
128 'progress_percentage' => round( $progress_percentage, 2 ),
129 'average_donation' => round( $average_donation, 2 ),
130 'largest_donation' => floatval( $largest_donation ),
131 'campaign_status' => $campaign_status,
132 'goal_type' => $goal_type,
133 'is_goal_reached' => ( $goal_amount > 0 && ( 'donation_count' === $goal_type ? intval( $donation_count ) >= $goal_amount : floatval( $total_raised ) >= $goal_amount ) ),
134 ];
135 }
136
137 /**
138 * Get recent donations for a campaign.
139 *
140 * @param int $campaign_id Campaign post ID.
141 * @param int $limit Number of donations to retrieve.
142 * @return array<int, array<string, mixed>>|null Recent donations.
143 * @since 0.0.1
144 */
145 public static function get_recent_donations( $campaign_id, $limit = 10 ) {
146 global $wpdb;
147
148 $donations_table = $wpdb->prefix . 'suredonation_donations';
149
150 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
151 return $wpdb->get_results(
152 $wpdb->prepare(
153 "SELECT id, (amount - refunded_amount) as amount, donor_name, donor_email, is_anonymous, created_at, payment_status
154 FROM %i
155 WHERE campaign_id = %d
156 AND payment_status IN ('completed', 'partially_refunded')
157 ORDER BY created_at DESC
158 LIMIT %d",
159 $donations_table,
160 $campaign_id,
161 $limit
162 ),
163 ARRAY_A
164 );
165 }
166
167 /**
168 * Get top donors for a campaign.
169 *
170 * @param int $campaign_id Campaign post ID.
171 * @param int $limit Number of donors to retrieve.
172 * @return array<int, array<string, mixed>>|null Top donors.
173 * @since 0.0.1
174 */
175 public static function get_top_donors( $campaign_id, $limit = 10 ) {
176 global $wpdb;
177
178 $donations_table = $wpdb->prefix . 'suredonation_donations';
179
180 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
181 return $wpdb->get_results(
182 $wpdb->prepare(
183 "SELECT
184 donor_email,
185 donor_name,
186 SUM(amount - refunded_amount) as total_donated,
187 COUNT(*) as donation_count,
188 MAX(created_at) as last_donation_date
189 FROM %i
190 WHERE campaign_id = %d
191 AND payment_status IN ('completed', 'partially_refunded')
192 AND is_anonymous = 0
193 AND donor_email IS NOT NULL
194 AND donor_email != ''
195 GROUP BY donor_email, donor_name
196 ORDER BY total_donated DESC
197 LIMIT %d",
198 $donations_table,
199 $campaign_id,
200 $limit
201 ),
202 ARRAY_A
203 );
204 }
205
206 /**
207 * Get donation timeline (grouped by date).
208 *
209 * @param int $campaign_id Campaign post ID.
210 * @param int $days Number of days to include.
211 * @return array<int, array<string, mixed>>|null Donation timeline.
212 * @since 0.0.1
213 */
214 public static function get_donation_timeline( $campaign_id, $days = 30 ) {
215 global $wpdb;
216
217 $donations_table = $wpdb->prefix . 'suredonation_donations';
218
219 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
220 return $wpdb->get_results(
221 $wpdb->prepare(
222 "SELECT
223 DATE(created_at) as date,
224 COUNT(*) as donation_count,
225 SUM(amount - refunded_amount) as total_amount
226 FROM %i
227 WHERE campaign_id = %d
228 AND payment_status IN ('completed', 'partially_refunded')
229 AND created_at >= DATE_SUB(NOW(), INTERVAL %d DAY)
230 GROUP BY DATE(created_at)
231 ORDER BY date ASC",
232 $donations_table,
233 $campaign_id,
234 $days
235 ),
236 ARRAY_A
237 );
238 }
239
240 /**
241 * Get cached recent donations for a campaign.
242 *
243 * Caches a fixed window per campaign (see LIST_CACHE_SIZE) and slices to
244 * the requested limit, so the public campaign-donations block doesn't hit
245 * the database on every page view.
246 *
247 * @param int $campaign_id Campaign post ID.
248 * @param int $limit Number of donations to return.
249 * @param int $cache_duration Cache duration in seconds (default: 5 minutes).
250 * @return array<int, array<string, mixed>> Recent donations.
251 * @since 1.0.0
252 */
253 public static function get_cached_recent_donations( $campaign_id, $limit = 10, $cache_duration = 300 ) {
254 $cache_key = 'suredonation_recent_donations_' . $campaign_id;
255 $donations = get_transient( $cache_key );
256
257 if ( false === $donations || ! is_array( $donations ) ) {
258 $donations = self::get_recent_donations( $campaign_id, self::LIST_CACHE_SIZE );
259 $donations = is_array( $donations ) ? $donations : [];
260 set_transient( $cache_key, $donations, $cache_duration );
261 }
262
263 return array_slice( $donations, 0, max( 0, (int) $limit ) );
264 }
265
266 /**
267 * Get cached top donors for a campaign.
268 *
269 * Caches a fixed window per campaign (see LIST_CACHE_SIZE) and slices to
270 * the requested limit, so the public campaign-donors block doesn't hit
271 * the database on every page view.
272 *
273 * @param int $campaign_id Campaign post ID.
274 * @param int $limit Number of donors to return.
275 * @param int $cache_duration Cache duration in seconds (default: 5 minutes).
276 * @return array<int, array<string, mixed>> Top donors.
277 * @since 1.0.0
278 */
279 public static function get_cached_top_donors( $campaign_id, $limit = 10, $cache_duration = 300 ) {
280 $cache_key = 'suredonation_top_donors_' . $campaign_id;
281 $donors = get_transient( $cache_key );
282
283 if ( false === $donors || ! is_array( $donors ) ) {
284 $donors = self::get_top_donors( $campaign_id, self::LIST_CACHE_SIZE );
285 $donors = is_array( $donors ) ? $donors : [];
286 set_transient( $cache_key, $donors, $cache_duration );
287 }
288
289 return array_slice( $donors, 0, max( 0, (int) $limit ) );
290 }
291
292 /**
293 * Clear campaign stats cache.
294 *
295 * @param int $campaign_id Campaign post ID.
296 * @return void
297 * @since 0.0.1
298 */
299 public static function clear_cache( $campaign_id ) {
300 delete_transient( 'suredonation_stats_' . $campaign_id );
301 delete_transient( 'suredonation_recent_donations_' . $campaign_id );
302 delete_transient( 'suredonation_top_donors_' . $campaign_id );
303 }
304
305 /**
306 * Get cached campaign statistics.
307 *
308 * @param int $campaign_id Campaign post ID.
309 * @param int $cache_duration Cache duration in seconds (default: 5 minutes).
310 * @return array<string, mixed> Campaign statistics.
311 * @since 0.0.1
312 */
313 public static function get_cached_stats( $campaign_id, $cache_duration = 300 ) {
314 $cache_key = 'suredonation_stats_' . $campaign_id;
315 $stats = get_transient( $cache_key );
316
317 if ( false === $stats || ! is_array( $stats ) ) {
318 $stats = self::get_stats( $campaign_id );
319 set_transient( $cache_key, $stats, $cache_duration );
320 }
321
322 return $stats;
323 }
324 }
325