PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.3.4
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.3.4
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.2 2.2.1 2.2.0 2.1.2 2.1.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 All 66 releases
better-payment / includes / WooCommerce / Emails.php

Emails.php in Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More 2.3.4, at includes/WooCommerce/Emails.php

362 lines 13.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Better_Payment\Lite\WooCommerce;
4
5 /**
6 * Exit if accessed directly
7 */
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Customer lifecycle emails for Better Payment subscriptions —
14 * "subscription cancelled" and "subscription completed".
15 *
16 * Built from the standard WooCommerce email pattern (WC_Email subclass +
17 * delayed send + reactivation guard), mapped onto Better Payment's own
18 * statuses:
19 *
20 * cancelled — the ONLY status with a reactivation route back to active
21 * (Subscriptions::reactivate() accepts cancelled/past_due), so
22 * its email is sent after a grace delay. Two guards keep a
23 * reactivated subscription from being told it is cancelled:
24 * subscription_reactivated unschedules the pending event, and
25 * the send callback re-checks the status is STILL 'cancelled'
26 * at send time — the re-check is the authoritative guard, the
27 * unschedule just avoids a pointless cron wake-up.
28 * completed — the subscription's schedule ended (the "expired" case in
29 * other subscription systems). No core flow completes a subscription
30 * any more (the renewal-cap feature was removed); the email
31 * remains for integrations that fire the action. Terminal with
32 * NO reactivation route, so the email sends immediately; a
33 * grace delay would protect nothing.
34 *
35 * The emails themselves are ordinary WC_Email registrations
36 * (Emails\SubscriptionCancelledEmail / Emails\SubscriptionCompletedEmail),
37 * so the shop owner enables/disables them and edits subject, heading and
38 * additional content from WooCommerce → Settings → Emails — no Better
39 * Payment settings are added. Both are enabled by default, like
40 * WooCommerce's own customer emails.
41 *
42 * This registrar is deliberately WC-free (no WC_Email reference at parse
43 * time) so the scheduling logic runs in the WooCommerce-less test
44 * environment; only the email classes themselves extend WC_Email, and they
45 * are instantiated solely inside the `woocommerce_email_classes` filter,
46 * which only WooCommerce fires.
47 *
48 * Only loaded when WooCommerce is active (see Loader::register()).
49 *
50 * @since 2.4.0
51 */
52 class Emails {
53
54 /**
55 * WC_Email ids (WooCommerce → Settings → Emails rows). Template file
56 * names derive from these — see template_html()/template_plain().
57 */
58 const CANCELLED_EMAIL_ID = 'bp_subscription_cancelled';
59 const COMPLETED_EMAIL_ID = 'bp_subscription_completed';
60
61 /**
62 * Single-event cron hook that sends the delayed cancelled email.
63 * Args: array( int $order_id ) — the id must stay a plain int, because
64 * unscheduling matches args by exact serialization.
65 */
66 const CANCELLED_SEND_HOOK = 'better_payment_woocommerce_send_cancelled_email';
67
68 /**
69 * Notification hooks the WC_Email classes bind their trigger() to.
70 * Fired through notify() after WC()->mailer() has instantiated the
71 * email classes.
72 */
73 const CANCELLED_NOTIFICATION = 'better_payment/woocommerce/subscription_cancelled_email_notification';
74 const COMPLETED_NOTIFICATION = 'better_payment/woocommerce/subscription_completed_email_notification';
75
76 /**
77 * Wire the feature. Called from Loader::register(), i.e. only when
78 * WooCommerce is active.
79 *
80 * @return void
81 */
82 public static function register() {
83 add_filter( 'woocommerce_email_classes', array( __CLASS__, 'register_email_classes' ) );
84
85 // Cancelled: schedule after a grace delay; reactivation unschedules.
86 add_action( 'better_payment/woocommerce/subscription_cancelled', array( __CLASS__, 'schedule_cancelled_email' ) );
87 add_action( 'better_payment/woocommerce/subscription_reactivated', array( __CLASS__, 'unschedule_cancelled_email' ) );
88 add_action( self::CANCELLED_SEND_HOOK, array( __CLASS__, 'send_cancelled_email' ) );
89
90 // Completed: terminal, no reactivation route — send immediately.
91 add_action( 'better_payment/woocommerce/subscription_completed', array( __CLASS__, 'send_completed_email' ) );
92
93 register_deactivation_hook( BETTER_PAYMENT_FILE, array( __CLASS__, 'unschedule_all' ) );
94 }
95
96 /**
97 * Clear every pending delayed-email event on plugin deactivation.
98 * wp_unschedule_hook() drops the hook's events regardless of args, so
99 * per-order events don't linger pointing at an unloaded listener.
100 *
101 * @return void
102 */
103 public static function unschedule_all() {
104 wp_unschedule_hook( self::CANCELLED_SEND_HOOK );
105 }
106
107 /**
108 * Filter callback for `woocommerce_email_classes`: add the two
109 * subscription lifecycle emails. The subclasses extend WC_Email, so
110 * they are only instantiated here — inside WooCommerce's own mailer
111 * bootstrap — never at module load.
112 *
113 * @param mixed $emails Registered email instances (id => WC_Email).
114 * @return mixed
115 */
116 public static function register_email_classes( $emails ) {
117 if ( ! is_array( $emails ) || ! class_exists( 'WC_Email' ) ) {
118 return $emails;
119 }
120
121 $emails[ self::CANCELLED_EMAIL_ID ] = new Emails\SubscriptionCancelledEmail();
122 $emails[ self::COMPLETED_EMAIL_ID ] = new Emails\SubscriptionCompletedEmail();
123
124 return $emails;
125 }
126
127 /* ---------------------------------------------------------------------
128 * Cancelled email — delayed send + reactivation guard
129 * ------------------------------------------------------------------- */
130
131 /**
132 * The grace delay (seconds) between a subscription being cancelled and
133 * the customer email going out. During the window a reactivation
134 * silently discards the email. 0 (or negative) sends immediately.
135 *
136 * @return int
137 */
138 public static function cancelled_email_delay() {
139 /**
140 * Filters the delay before the "subscription cancelled" customer
141 * email is sent. Return 0 to send immediately.
142 *
143 * @since 2.4.0
144 *
145 * @param int $delay Delay in seconds. Default HOUR_IN_SECONDS.
146 */
147 return (int) apply_filters( 'better_payment/woocommerce/cancelled_email_delay', HOUR_IN_SECONDS );
148 }
149
150 /**
151 * Consume `better_payment/woocommerce/subscription_cancelled`: queue the
152 * customer email after the grace delay. Re-cancelling (cancel →
153 * reactivate → cancel) replaces any pending event rather than stacking
154 * a second one.
155 *
156 * @param mixed $order Parent (subscription) order.
157 * @return void
158 */
159 public static function schedule_cancelled_email( $order ) {
160 $order_id = self::order_id( $order );
161
162 if ( $order_id < 1 ) {
163 return;
164 }
165
166 $delay = self::cancelled_email_delay();
167
168 if ( $delay <= 0 ) {
169 self::send_cancelled_email( $order_id );
170 return;
171 }
172
173 // Dedupe: exactly one pending event per subscription.
174 wp_clear_scheduled_hook( self::CANCELLED_SEND_HOOK, array( $order_id ) );
175 wp_schedule_single_event( time() + $delay, self::CANCELLED_SEND_HOOK, array( $order_id ) );
176 }
177
178 /**
179 * Consume `better_payment/woocommerce/subscription_reactivated`: the
180 * subscription is live again — discard any pending cancelled email.
181 *
182 * @param mixed $order Parent (subscription) order.
183 * @return void
184 */
185 public static function unschedule_cancelled_email( $order ) {
186 $order_id = self::order_id( $order );
187
188 if ( $order_id < 1 ) {
189 return;
190 }
191
192 wp_clear_scheduled_hook( self::CANCELLED_SEND_HOOK, array( $order_id ) );
193 }
194
195 /**
196 * Send the "subscription cancelled" email — the delayed cron callback
197 * (and the immediate path when the delay is 0).
198 *
199 * The status re-check is the authoritative reactivation guard: even if
200 * an unschedule was missed, a subscription that is no longer
201 * 'cancelled' is never emailed about a cancellation.
202 *
203 * @param mixed $order_id Parent (subscription) order id.
204 * @return void
205 */
206 public static function send_cancelled_email( $order_id ) {
207 $order_id = absint( $order_id );
208
209 if ( $order_id < 1 || ! function_exists( 'wc_get_order' ) ) {
210 return;
211 }
212
213 $order = wc_get_order( $order_id );
214
215 if ( ! $order instanceof \WC_Order ) {
216 return;
217 }
218
219 if ( ! self::should_send( (string) $order->get_meta( Subscriptions::STATUS_META ), 'cancelled' ) ) {
220 return;
221 }
222
223 self::notify( self::CANCELLED_NOTIFICATION, $order_id );
224 }
225
226 /* ---------------------------------------------------------------------
227 * Completed email — immediate
228 * ------------------------------------------------------------------- */
229
230 /**
231 * Consume `better_payment/woocommerce/subscription_completed`: the
232 * subscription ended (fired by integrations only — no core flow
233 * completes a subscription). Terminal with no reactivation route, so
234 * the email goes out immediately — a delay would guard nothing.
235 *
236 * @param mixed $order Parent (subscription) order.
237 * @return void
238 */
239 public static function send_completed_email( $order ) {
240 $order_id = self::order_id( $order );
241
242 if ( $order_id < 1 || ! function_exists( 'wc_get_order' ) ) {
243 return;
244 }
245
246 self::notify( self::COMPLETED_NOTIFICATION, $order_id );
247 }
248
249 /**
250 * Fire a notification hook with the mailer loaded. WC()->mailer()
251 * instantiates every registered email class (running our
252 * `woocommerce_email_classes` filter), whose constructors bind
253 * trigger() to these hooks — without it the do_action falls on nothing.
254 *
255 * @param string $hook Notification hook name.
256 * @param int $order_id Parent (subscription) order id.
257 * @return void
258 */
259 protected static function notify( $hook, $order_id ) {
260 if ( ! function_exists( 'WC' ) ) {
261 return;
262 }
263
264 WC()->mailer();
265
266 do_action( $hook, $order_id );
267 }
268
269 /* ---------------------------------------------------------------------
270 * Pure helpers (unit-tested without WooCommerce)
271 * ------------------------------------------------------------------- */
272
273 /**
274 * Pure: the order id from a WC_Order-ish object or a numeric id.
275 * Tolerant on purpose — the lifecycle hooks pass WC_Order, tests and
276 * the cron path pass plain ints.
277 *
278 * @param mixed $order Order object or id.
279 * @return int 0 when no id can be resolved.
280 */
281 public static function order_id( $order ) {
282 if ( is_object( $order ) && method_exists( $order, 'get_id' ) ) {
283 return (int) $order->get_id();
284 }
285
286 if ( is_numeric( $order ) ) {
287 return (int) $order;
288 }
289
290 return 0;
291 }
292
293 /**
294 * Pure: whether a lifecycle email may still be sent — the subscription's
295 * current status must be exactly the status the email announces. This is
296 * what keeps a reactivated subscription from receiving a stale
297 * "cancelled" email.
298 *
299 * @param string $current Current `_bp_subscription_status` meta value.
300 * @param string $required Status the email announces.
301 * @return bool
302 */
303 public static function should_send( $current, $required ) {
304 return '' !== (string) $required && (string) $current === (string) $required;
305 }
306
307 /**
308 * Pure: the plugin template root the email templates resolve against.
309 *
310 * @return string
311 */
312 public static function template_base() {
313 return ( defined( 'BETTER_PAYMENT_PATH' ) ? BETTER_PAYMENT_PATH : '' ) . 'templates/';
314 }
315
316 /**
317 * Pure: HTML template path (relative to template_base()) for an email id.
318 * Ids are underscored (`bp_subscription_cancelled`), files hyphenated
319 * (`emails/bp-subscription-cancelled.php`) per WooCommerce convention.
320 *
321 * @param string $email_id WC_Email id.
322 * @return string
323 */
324 public static function template_html( $email_id ) {
325 return 'emails/' . str_replace( '_', '-', (string) $email_id ) . '.php';
326 }
327
328 /**
329 * Pure: plain-text template path for an email id.
330 *
331 * @param string $email_id WC_Email id.
332 * @return string
333 */
334 public static function template_plain( $email_id ) {
335 return 'emails/plain/' . str_replace( '_', '-', (string) $email_id ) . '.php';
336 }
337
338 /**
339 * The customer-facing URL for a subscription: the My Account →
340 * Subscriptions single view when the tab is enabled, else WooCommerce's
341 * own view-order page (which always exists for a logged-in customer).
342 *
343 * @param mixed $order Parent (subscription) order.
344 * @return string '' when no URL can be built.
345 */
346 public static function subscription_view_url( $order ) {
347 if ( ! $order instanceof \WC_Order ) {
348 return '';
349 }
350
351 if ( 'yes' === Subscriptions::setting( 'myaccount_tab' ) ) {
352 $url = (string) MyAccount::view_url( $order->get_id() );
353
354 if ( '' !== $url ) {
355 return $url;
356 }
357 }
358
359 return (string) $order->get_view_order_url();
360 }
361 }
362