PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 1.7.0.9 All 211 releases
charitable / includes / admin / notifications / class-charitable-notification-triggers.php

class-charitable-notification-triggers.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13, at includes/admin/notifications/class-charitable-notification-triggers.php

343 lines 10.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable Notification Triggers.
4 *
5 * Hooks into donation/campaign events to fire one-time milestone
6 * and lifecycle notifications. All handlers are try/catch wrapped
7 * at priority 9999 — donation processing is fully complete before
8 * any of this code runs and any exception is silently logged.
9 *
10 * @package Charitable/Classes/Charitable_Notification_Triggers
11 * @since 1.8.12
12 */
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 class Charitable_Notification_Triggers {
19
20 private static $instance = null;
21
22 /** Donor count milestones to fire notifications at. */
23 const DONOR_MILESTONES = [ 1, 5, 10, 50, 100 ];
24
25 public static function get_instance() {
26 if ( is_null( self::$instance ) ) {
27 self::$instance = new self();
28 }
29 return self::$instance;
30 }
31
32 public function init() {
33 // Donation completed — milestones + goal check.
34 add_action(
35 'charitable_donation_status_charitable-completed',
36 function( $donation, $old_status ) {
37 try {
38 self::get_instance()->handle_donation_completed( $donation );
39 } catch ( Exception $e ) {
40 if ( charitable_is_debug() ) { error_log( 'Charitable notification trigger error (donation): ' . $e->getMessage() ); }
41 }
42 },
43 9999,
44 2
45 );
46
47 // Campaign status transitions — lifecycle events.
48 add_action(
49 'transition_post_status',
50 function( $new_status, $old_status, $post ) {
51 if ( 'campaign' !== get_post_type( $post ) ) {
52 return;
53 }
54 try {
55 self::get_instance()->handle_campaign_status_change( $new_status, $old_status, $post );
56 } catch ( Exception $e ) {
57 if ( charitable_is_debug() ) { error_log( 'Charitable notification trigger error (campaign): ' . $e->getMessage() ); }
58 }
59 },
60 9999,
61 3
62 );
63 }
64
65 /**
66 * Handle donation completed event.
67 *
68 * @param Charitable_Donation $donation
69 */
70 public function handle_donation_completed( $donation ) {
71 // Exclude test mode donations.
72 if ( $donation->get( 'test_mode' ) ) {
73 return;
74 }
75
76 $this->check_donor_milestones( $donation );
77 $this->check_largest_donation( $donation );
78 $this->check_campaign_goal( $donation );
79 }
80
81 /**
82 * Handle campaign status change.
83 *
84 * @param string $new_status
85 * @param string $old_status
86 * @param WP_Post $post
87 */
88 public function handle_campaign_status_change( $new_status, $old_status, $post ) {
89 $local = Charitable_Local_Notifications::get_instance();
90
91 // Campaign went live.
92 if ( 'publish' === $new_status && 'publish' !== $old_status ) {
93 $local->add(
94 'lifecycle_campaign_live_' . $post->ID,
95 [
96 'title' => sprintf(
97 /* translators: %s: campaign title */
98 __( '"%s" is now live', 'charitable' ),
99 get_the_title( $post )
100 ),
101 'content' => '<p>' . __( 'Your campaign is published and accepting donations.', 'charitable' ) . '</p>',
102 'notification_type' => 'info',
103 'dismissible' => true,
104 'badge_label' => __( 'Campaign', 'charitable' ),
105 'badge_type' => 'info',
106 'category' => 'lifecycle',
107 'btns' => [
108 'main' => [
109 'url' => get_permalink( $post->ID ),
110 'text' => __( 'View Campaign', 'charitable' ),
111 ],
112 ],
113 ]
114 );
115 }
116
117 // Campaign ended (transitioned away from publish).
118 if ( 'publish' === $old_status && 'publish' !== $new_status ) {
119 $campaign = new Charitable_Campaign( $post );
120 $goal = $campaign->get_goal();
121 $raised = $campaign->get_donated_amount();
122 $met_goal = $goal && $raised >= $goal;
123
124 $local->add(
125 'lifecycle_campaign_ended_' . $post->ID,
126 [
127 'title' => sprintf(
128 /* translators: %s: campaign title */
129 $met_goal
130 ? __( '"%s" ended — goal met!', 'charitable' )
131 : __( '"%s" has ended', 'charitable' ),
132 get_the_title( $post )
133 ),
134 'content' => '<p>' . sprintf(
135 /* translators: %s: amount raised (wrapped in stat span) */
136 __( 'Total raised: %s.', 'charitable' ),
137 '<span class="charitable-notif-stat">' . charitable_format_money( $raised ) . '</span>'
138 ) . '</p>',
139 'notification_type' => $met_goal ? 'success' : 'info',
140 'dismissible' => true,
141 'badge_label' => __( 'Campaign', 'charitable' ),
142 'badge_type' => $met_goal ? 'success' : 'info',
143 'category' => 'lifecycle',
144 'btns' => [
145 'main' => [
146 'url' => admin_url( 'post.php?action=edit&post=' . $post->ID ),
147 'text' => __( 'View Campaign', 'charitable' ),
148 ],
149 ],
150 ]
151 );
152 }
153 }
154
155 /**
156 * Check if a donor count milestone has just been crossed.
157 *
158 * @param Charitable_Donation $donation
159 */
160 private function check_donor_milestones( $donation ) {
161 $local = Charitable_Local_Notifications::get_instance();
162 $total_donors = $this->get_unique_real_donor_count();
163
164 foreach ( self::DONOR_MILESTONES as $milestone ) {
165 $id = 'milestone_donors_' . $milestone;
166
167 // Already fired?
168 $notifications = Charitable_Notifications::get_instance();
169 $option = $notifications->get_option();
170 $already_fired = false;
171 foreach ( $option['local'] as $item ) {
172 if ( isset( $item['id'] ) && $item['id'] === $id ) {
173 $already_fired = true;
174 break;
175 }
176 }
177 if ( $already_fired ) {
178 continue;
179 }
180
181 if ( $total_donors >= $milestone ) {
182 if ( 1 === $milestone ) {
183 $title = __( 'You received your first donation!', 'charitable' );
184 $content = '<p>' . __( 'Your first real donation has been received. Congratulations!', 'charitable' ) . '</p>';
185 } else {
186 $title = sprintf(
187 /* translators: %d: donor count */
188 __( "You've reached %d unique donors!", 'charitable' ),
189 $milestone
190 );
191 $content = '<p>' . sprintf(
192 /* translators: %s: donor count (wrapped in stat span) */
193 __( '%s real donors have contributed to your campaigns.', 'charitable' ),
194 '<span class="charitable-notif-stat">' . number_format_i18n( $milestone ) . '</span>'
195 ) . '</p>';
196 }
197
198 $local->add( $id, [
199 'title' => $title,
200 'content' => $content,
201 'notification_type' => 'success',
202 'dismissible' => true,
203 'badge_label' => __( 'Milestone', 'charitable' ),
204 'badge_type' => 'success',
205 'category' => 'milestone',
206 'btns' => [
207 'main' => [
208 'url' => admin_url( 'admin.php?page=charitable-donors' ),
209 'text' => __( 'View Donors', 'charitable' ),
210 ],
211 ],
212 ] );
213 break; // Only fire the highest newly-crossed milestone per donation.
214 }
215 }
216 }
217
218 /**
219 * Check if this donation is the largest ever received.
220 *
221 * @param Charitable_Donation $donation
222 */
223 private function check_largest_donation( $donation ) {
224 $amount = $donation->get_total_donation_amount( true );
225 $stored_max = (float) get_option( 'charitable_notifications_largest_donation', 0 );
226
227 if ( $amount <= $stored_max ) {
228 return;
229 }
230
231 update_option( 'charitable_notifications_largest_donation', $amount );
232
233 if ( $stored_max > 0 ) { // Don't fire on the very first donation (covered by donor milestone).
234 Charitable_Local_Notifications::get_instance()->add(
235 'milestone_largest_donation',
236 [
237 'title' => __( 'New largest single donation received!', 'charitable' ),
238 'content' => '<p>' . sprintf(
239 /* translators: %s: formatted amount (wrapped in stat span) */
240 __( 'A new record: %s in a single donation.', 'charitable' ),
241 '<span class="charitable-notif-stat">' . charitable_format_money( $amount ) . '</span>'
242 ) . '</p>',
243 'notification_type' => 'success',
244 'dismissible' => true,
245 'badge_label' => __( 'Milestone', 'charitable' ),
246 'badge_type' => 'success',
247 'category' => 'milestone',
248 'btns' => [
249 'main' => [
250 'url' => admin_url( 'post.php?action=edit&post=' . $donation->get_donation_id() ),
251 'text' => __( 'View Donation', 'charitable' ),
252 ],
253 ],
254 ]
255 );
256 }
257 }
258
259 /**
260 * Check if the campaign associated with this donation has just reached its goal.
261 *
262 * @param Charitable_Donation $donation
263 */
264 private function check_campaign_goal( $donation ) {
265 $campaign_donations = $donation->get_campaign_donations(); // returns array of objects with ->campaign_id
266 if ( empty( $campaign_donations ) ) {
267 return;
268 }
269
270 $local = Charitable_Local_Notifications::get_instance();
271
272 foreach ( $campaign_donations as $cd ) {
273 $campaign_id = isset( $cd->campaign_id ) ? (int) $cd->campaign_id : 0;
274 if ( ! $campaign_id ) {
275 continue;
276 }
277
278 $campaign = new Charitable_Campaign( get_post( $campaign_id ) );
279
280 if ( ! $campaign->has_goal() || ! $campaign->has_achieved_goal() ) {
281 continue;
282 }
283
284 $notif_id = 'lifecycle_campaign_goal_' . $campaign_id;
285
286 // Already fired for this campaign?
287 $notifications = Charitable_Notifications::get_instance();
288 $option = $notifications->get_option();
289 foreach ( $option['local'] as $item ) {
290 if ( isset( $item['id'] ) && $item['id'] === $notif_id ) {
291 continue 2; // Already recorded.
292 }
293 }
294
295 $local->add( $notif_id, [
296 'title' => sprintf(
297 /* translators: %s: campaign title */
298 __( '"%s" has reached its goal!', 'charitable' ),
299 get_the_title( $campaign_id )
300 ),
301 'content' => '<p>' . sprintf(
302 /* translators: %s: goal amount (wrapped in stat span) */
303 __( 'Your campaign hit its %s target.', 'charitable' ),
304 '<span class="charitable-notif-stat">' . charitable_format_money( $campaign->get_goal() ) . '</span>'
305 ) . '</p>',
306 'notification_type' => 'celebration',
307 'dismissible' => true,
308 'badge_label' => __( 'Campaign', 'charitable' ),
309 'badge_type' => 'success',
310 'category' => 'lifecycle',
311 'btns' => [
312 'main' => [
313 'url' => admin_url( 'post.php?action=edit&post=' . $campaign_id ),
314 'text' => __( 'View Campaign', 'charitable' ),
315 ],
316 ],
317 ] );
318 }
319 }
320
321 /**
322 * Get total unique real (non-test) donor count site-wide.
323 *
324 * @return int
325 */
326 private function get_unique_real_donor_count() {
327 global $wpdb;
328
329 return (int) $wpdb->get_var( // phpcs:ignore
330 "SELECT COUNT(DISTINCT donor_id)
331 FROM {$wpdb->prefix}charitable_campaign_donations cd
332 INNER JOIN {$wpdb->posts} p ON p.ID = cd.donation_id
333 WHERE p.post_status = 'charitable-completed'
334 AND NOT EXISTS (
335 SELECT 1 FROM {$wpdb->postmeta} pm
336 WHERE pm.post_id = p.ID
337 AND pm.meta_key = '_test_mode'
338 AND pm.meta_value = '1'
339 )"
340 );
341 }
342 }
343