PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.7.0.7
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.7.0.7
1.8.13.1 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 All 212 releases
charitable / includes / stripe / gateway / class-charitable-stripe-webhook-processor.php

class-charitable-stripe-webhook-processor.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.7.0.7, at includes/stripe/gateway/class-charitable-stripe-webhook-processor.php

975 lines 31.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class responsible for processing webhooks.
4 *
5 * @package Charitable Stripe/Classes/Charitable_Stripe_Webhook_Processor
6 * @author Eric Daams
7 * @copyright Copyright (c) 2021-2022, Studio 164a
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.3.0
10 * @version 1.4.13
11 */
12
13 // Exit if accessed directly.
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 if ( ! class_exists( 'Charitable_Stripe_Webhook_Processor' ) ) :
19
20 /**
21 * Charitable_Stripe_Webhook_Processor
22 *
23 * @since 1.3.0
24 */
25 class Charitable_Stripe_Webhook_Processor {
26
27 /**
28 * Event object.
29 *
30 * @since 1.3.0
31 *
32 * @var \Stripe\Event
33 */
34 protected $event;
35
36 /**
37 * Gateway helper.
38 *
39 * @since 1.3.0
40 *
41 * @var Charitable_Gateway_Stripe_AM
42 */
43 protected $gateway;
44
45 /**
46 * Stripe Event object.
47 *
48 * @deprecated
49 *
50 * @since 1.3.0
51 * @since 1.4.0 Deprecated.
52 *
53 * @var \Stripe\Event
54 */
55 protected $stripe_event;
56
57 /**
58 * Create class object.
59 *
60 * @since 1.3.0
61 *
62 * @param \Stripe\Event $event Incoming event object.
63 *
64 */
65 public function __construct( \Stripe\Event $event ) {
66 $this->event = $event;
67 $this->gateway = new Charitable_Gateway_Stripe_AM();
68 }
69
70 /**
71 * Process an incoming Stripe IPN.
72 *
73 * @since 1.3.0
74 *
75 * @return void
76 */
77 public static function process() {
78
79 error_log( 'Charitable_Stripe_Webhook_Processor PROCESS FUNCTION ');
80
81 /* Retrieve and validate the request's body. */
82 $event = self::get_validated_incoming_event();
83
84 error_log( print_r( $event, true ) );
85
86 if ( ! $event ) {
87 status_header( 500 );
88 die( __( 'Invalid Stripe event.', 'charitable-stripe' ) );
89 }
90
91 try {
92 error_log( 'Charitable_Stripe_Webhook_Processor PROCESS FUNCTION TRY');
93 $event = \Stripe\Event::constructFrom( $event );
94 } catch( \UnexpectedValueException $e ) {
95 status_header( 400 );
96 die( __( 'Unable to construct Stripe object with payload.', 'charitable-stripe' ) );
97 }
98
99 $processor = new Charitable_Stripe_Webhook_Processor( $event );
100 error_log( 'Charitable_Stripe_Webhook_Processor PROCESS FUNCTION RUN');
101 $processor->run();
102 }
103
104 /**
105 * Run the processor.
106 *
107 * @since 1.3.0
108 *
109 * @return void
110 */
111 public function run() {
112 $this->set_stripe_api_key();
113
114 try {
115 status_header( 200 );
116
117 /* This is Stripe's test webhook, so just die with a success message. */
118 if ( 'evt_00000000000000' == $this->event->id ) {
119 die( __( 'Test webhook successfully received.', 'charitable-stripe' ) );
120 }
121
122 $this->run_event_processors();
123
124 die( __( 'Webhook processed.', 'charitable-stripe' ) );
125
126 } catch ( Exception $e ) {
127 $body = $e->getJsonBody();
128 error_log( $body['error']['message'] );
129 status_header( 500 );
130
131 die( __( 'Error while retrieving event.', 'charitable-stripe' ) );
132 }//end try
133 }
134
135 /**
136 * Set Stripe API key.
137 *
138 * @since 1.3.0
139 *
140 * @return boolean True if the API key is set. False otherwise.
141 */
142 public function set_stripe_api_key() {
143 $keys = $this->gateway->get_keys( false === $this->event->livemode );
144
145 if ( empty( $keys['secret_key'] ) ) {
146 return false;
147 }
148
149 return $this->gateway->setup_api( $keys['secret_key'] );
150 }
151
152 /**
153 * Get the account ID for the site.
154 *
155 * @since 1.3.0
156 *
157 * @return string|null Account ID if successfull. Null if the account couldn't be retrieved from Stripe.
158 */
159 public function get_site_account_id() {
160 $account_id = $this->gateway->get_value( 'account_id' );
161
162 if ( empty( $account_id ) ) {
163 try {
164 $this->set_stripe_api_key();
165
166 $account = \Stripe\Account::retrieve();
167 $account_id = $account->id;
168
169 /* Store the account id in the gateway settings. */
170 $options = get_option( 'charitable_settings' );
171 $options['gateways_stripe']['account_id'] = $account_id;
172
173 update_option( 'charitable_settings', $options );
174
175 } catch ( Exception $e ) {
176 $account_id = null;
177 }
178 }
179
180 return $account_id;
181 }
182
183 /**
184 * Check whether the current event is from a Connect webhook and is signed for the platform account.
185 *
186 * @since 1.3.0
187 *
188 * @return boolean
189 */
190 public function is_connect_webhook_for_site_account_id() {
191 error_log( 'is_connect_webhook_for_site_account_id' );
192 error_log( print_r( $this->event->account, true ) );
193 error_log( print_r( $this->get_site_account_id(), true ) );
194 error_log( print_r( isset( $this->event->account ) && $this->get_site_account_id() == $this->event->account, true ) );
195 return isset( $this->event->account ) && $this->get_site_account_id() == $this->event->account;
196 }
197
198 /**
199 * Checks whether the current event is from a Connect webhook and
200 * is for a transaction taking place directly on a connected account.
201 *
202 * @since 1.4.0
203 *
204 * @return boolean
205 */
206 public function is_connect_webhook_for_connected_account() {
207 return isset( $this->event->account ) && $this->get_site_account_id() != $this->event->account;
208 }
209
210 /**
211 * Get the options array to pass when retrieving the event from Stripe.
212 *
213 * @since 1.3.0
214 *
215 * @return array
216 */
217 public function get_options() {
218 if ( isset( $this->event->account ) ) {
219 return [
220 'stripe_account' => $this->event->account,
221 ];
222 }
223
224 return [];
225 }
226
227 /**
228 * Sets up any default event processors.
229 *
230 * @since 1.3.0
231 *
232 * @return void
233 */
234 public function run_event_processors() {
235 /**
236 * Default event processors.
237 *
238 * @since 1.3.0
239 *
240 * @param array $processors Array of Stripe event types and associated callback functions.
241 */
242 $default_processors = apply_filters(
243 'charitable_stripe_default_event_processors',
244 [
245 'charge.refunded' => [ $this, 'process_refund' ],
246 'invoice.created' => [ $this, 'process_invoice_created' ],
247 'invoice.payment_failed' => [ $this, 'process_invoice_payment_failed' ],
248 'invoice.payment_succeeded' => [ $this, 'process_invoice_payment_succeeded' ],
249 'customer.subscription.updated' => [ $this, 'process_customer_subscription_updated' ],
250 'customer.subscription.deleted' => [ $this, 'process_customer_subscription_deleted' ],
251 'payment_intent.payment_failed' => [ $this, 'process_payment_intent_payment_failed' ],
252 'payment_intent.succeeded' => [ $this, 'process_payment_intent_succeeded' ],
253 'checkout.session.completed' => [ $this, 'process_checkout_session_completed' ],
254 ]
255 );
256
257 error_log( 'run_event_processors');
258 error_log( 'default_processors');
259 error_log( print_r( $default_processors, true ) );
260 error_log( '$this->event->type');
261 error_log( print_r( $this->event->type, true ) );
262
263 /* Check if this event can be handled by one of our built-in event processors. */
264 if ( array_key_exists( $this->event->type, $default_processors ) ) {
265
266 error_log( 'array_key_exists');
267
268 /**
269 * Double-check that this isn't a Connect webhook for the site account.
270 *
271 * We want to skip processing for those because there will be a duplicate
272 * standard webhook coming in as well, for the same event.
273 *
274 * If you still want to do something with the Connect webhook, you can use
275 * the `charitable_stripe_ipn_event` hook below.
276 */
277 if ( ! $this->is_connect_webhook_for_site_account_id() ) {
278
279 error_log( '! $this->is_connect_webhook_for_site_account_id()');
280
281 $message = call_user_func( $default_processors[ $this->event->type ], $this->event );
282
283 /* Kill processing with a message returned by the event processor. */
284 die( $message );
285 }
286 }
287
288 /**
289 * Fire an action hook to process the event.
290 *
291 * Note that this will only fire for webhooks that have not already been processed by one
292 * of the default webhook handlers above.
293 *
294 * @since 1.0.0
295 *
296 * @param string $event_type Type of event.
297 * @param \Stripe\Event $event Stripe event object.
298 */
299 error_log( 'before charitable_stripe_ipn_event');
300 do_action( 'charitable_stripe_ipn_event', $this->event->type, $this->event );
301 error_log( 'after charitable_stripe_ipn_event');
302 }
303
304 /**
305 * Process a refund initiated via the Stripe dashboard.
306 *
307 * @see https://stripe.com/docs/api#events
308 *
309 * @since 1.3.0
310 *
311 * @param object $event The Stripe event object.
312 * @return string Response message
313 */
314 public function process_refund( $event ) {
315 $charge = $event->data->object;
316
317 /**
318 * If we're missing a donation ID, stop processing.
319 * This probably isn't a Charitable payment.
320 */
321 if ( ! isset( $charge->metadata->donation_id ) ) {
322 return __( 'Donation Webhook: Missing donation ID', 'charitable-stripe' );
323 }
324
325 $donation_id = $charge->metadata->donation_id;
326 $refund = $charge->refunds->data[0];
327 $refund_amount = $refund->amount;
328
329 if ( ! Charitable_Stripe_Gateway_Processor::is_zero_decimal_currency( $refund->currency ) ) {
330 $refund_amount = $refund_amount / 100;
331 }
332
333 if ( Charitable::DONATION_POST_TYPE !== get_post_type( $donation_id ) ) {
334 return __( 'Donation Webhook: Refund donation ID not valid', 'charitable-stripe' );
335 }
336
337 $donation = new Charitable_Donation( $donation_id );
338
339 /**
340 * Ensure that the gateway transaction ID matches the charge ID, to avoid refunding a
341 * donation originally made on a different site.
342 *
343 * @see https://bitbucket.org/wpcharitable/charitable-stripe/issues/54/webhooks-distinguish-between-webhooks-for
344 */
345 if ( $donation->get_gateway_transaction_id() != $charge->id ) {
346 return __( 'Donation Webhook: Charge ID does not match donation reference on this site', 'charitable-stripe' );
347 }
348
349 $donation->process_refund( $refund_amount, __( 'Donation refunded from the Stripe dashboard.', 'charitable-stripe' ) );
350
351 return __( 'Donation Webhook: Refund processed', 'charitable-stripe' );
352 }
353
354 /**
355 * Process the payment_intent.payment_failed webhook.
356 *
357 * @since 1.4.0
358 *
359 * @param object $event The Stripe event object.
360 * @return string Response message.
361 */
362 public function process_payment_intent_payment_failed( $event ) {
363 $payment_intent = $event->data->object;
364
365 /* Process a failed payment intent for a subscription payment. */
366 if ( ! is_null( $payment_intent->invoice ) ) {
367 return $this->process_payment_intent_payment_failed_for_subscription( $event );
368 }
369
370 if ( ! isset( $payment_intent->metadata->donation_id ) ) {
371 return __( 'Donation Webhook: Missing donation ID', 'charitable-stripe' );
372 }
373
374 $donation_id = $payment_intent->metadata->donation_id;
375
376 if ( Charitable::DONATION_POST_TYPE !== get_post_type( $donation_id ) ) {
377 return __( 'Donation Webhook: Donation ID not valid', 'charitable-stripe' );
378 }
379
380 $donation = new Charitable_Donation( $donation_id );
381
382 /**
383 * Ensure that the payment intent matches the one we have on record for this
384 * donation, to make sure this is the correct donation.
385 *
386 * @see https://github.com/Charitable/Charitable-Stripe/issues/54/
387 */
388 if ( get_post_meta( $donation_id, '_stripe_payment_intent', true ) != $payment_intent->id ) {
389 return __( 'Donation Webhook: Payment Intent does not match donation reference on this site', 'charitable-stripe' );
390 }
391
392 /* Log the payment error along with the error code. */
393 $donation->log()->add(
394 sprintf(
395 '%1$s Error code: <a href="%2$s" target="_blank">%3$s</a>',
396 $payment_intent->last_payment_error->message,
397 $payment_intent->last_payment_error->doc_url,
398 $payment_intent->last_payment_error->code
399 )
400 );
401
402 /* Record the number of payment failures. */
403 $this->update_payment_failure_count( $donation, $payment_intent->id );
404
405 /* Mark the donation as Failed. */
406 $donation->update_status( 'charitable-failed' );
407
408 return __( 'Donation Webhook: Donation marked as Failed', 'charitable-stripe' );
409 }
410
411 /**
412 * Process a payment intent payment failure for a subscription payment.
413 *
414 * @since 1.4.0
415 *
416 * @param object $event The Stripe event object.
417 * @return string
418 */
419 public function process_payment_intent_payment_failed_for_subscription( $event ) {
420 if ( ! $this->is_recurring_installed() ) {
421 return __( 'Subscription Webhook: Unable to process without Charitable Recurring extension.', 'charitable-stripe' );
422 }
423
424 $payment_intent = $event->data->object;
425
426 try {
427 $gateway = new Charitable_Gateway_Stripe_AM;
428 $gateway->setup_api();
429
430 /* Get the invoice, so we can get the subscription id from that. */
431 $invoice = \Stripe\Invoice::retrieve( $payment_intent->invoice, $this->get_options() );
432 } catch ( Exception $e ) {
433 return __( 'Donation Webhook: Unable to retrieve invoice for failed payment intent.', 'charitable-stripe' );
434 }
435
436 $subscription = charitable_recurring_get_subscription_by_gateway_id( $invoice->subscription, 'stripe' );
437
438 if ( ! $subscription || ! is_a( $subscription, 'Charitable_Recurring_Donation' ) ) {
439 return __( 'Donation Webhook: No matching subscription found for invoice with failed payment intent.', 'charitable-stripe' );
440 }
441
442 $subscription_log = new Charitable_Stripe_Recurring_Donation_Log( $subscription );
443 $first_donation = $subscription->get_first_donation_id();
444
445 /* Make sure this is not for the first donation. */
446 if ( 'charitable-pending' != get_post_status( $first_donation ) ) {
447 $subscription_log->log_failed_renewal_invoice( $invoice->id, $invoice->payment_intent );
448
449 /* Mark the subscription as cancelled. */
450 if ( 'canceled' == $payment_intent->status ) {
451 $subscription->update_status( 'charitable-cancelled' );
452 return __( 'Donation Webhook: Recurring donation for payment intent marked as cancelled.', 'charitable-stripe' );
453 } else {
454 $subscription->update_status( 'charitable-cancel' );
455 return __( 'Donation Webhook: Recurring donation for payment intent marked as pending cancellation.', 'charitable-stripe' );
456 }
457 }
458
459 /* This was the first donation, so mark the recurring donation as failed. */
460 $subscription->set_to_failed(
461 $subscription_log->get_failed_invoice_log_message( $invoice->id, $invoice->payment_intent )
462 );
463
464 /* Log the payment error along with the error code. */
465 $donation = new Charitable_Donation( $first_donation );
466 $donation->log()->add(
467 sprintf(
468 '%1$s Error code: <a href="%2$s" target="_blank">%3$s</a>',
469 $payment_intent->last_payment_error->message,
470 $payment_intent->last_payment_error->doc_url,
471 $payment_intent->last_payment_error->code
472 )
473 );
474
475 /* Record the number of payment failures. */
476 $this->update_payment_failure_count( $donation, $invoice->payment_intent );
477
478 /* Mark the donation as Failed. */
479 $donation->update_status( 'charitable-failed' );
480
481 return __( 'Donation Webhook: Recurring donation and initial payment for payment intent marked as failed'. 'charitable-stripe' );
482 }
483
484 /**
485 * Process the payment_intent.succeeded webhook.
486 *
487 * @since 1.4.0
488 *
489 * @param object $event The Stripe event object.
490 * @return string Response message.
491 */
492 public function process_payment_intent_succeeded( $event ) {
493
494 error_log('process_payment_intent_succeeded');
495
496 $payment_intent = $event->data->object;
497
498 error_log( print_r( $payment_intent, true ) );
499
500 if ( ! isset( $payment_intent->metadata->donation_id ) ) {
501 return __( 'Donation Webhook: Missing donation ID', 'charitable-stripe' );
502 }
503
504 $donation_id = $payment_intent->metadata->donation_id;
505
506 error_log( print_r( $donation_id, true ) );
507
508 if ( Charitable::DONATION_POST_TYPE !== get_post_type( $donation_id ) ) {
509 return __( 'Donation Webhook: Donation ID not valid', 'charitable-stripe' );
510 }
511
512 /**
513 * Ensure that the payment intent matches the one we have on record for this
514 * donation, to make sure this is the correct donation.
515 *
516 * @see https://bitbucket.org/wpcharitable/charitable-stripe/issues/54/webhooks-distinguish-between-webhooks-for
517 */
518 if ( get_post_meta( $donation_id, '_stripe_payment_intent', true ) != $payment_intent->id ) {
519 return __( 'Donation Webhook: Payment Intent does not match donation reference on this site', 'charitable-stripe' );
520 }
521
522 $donation = new Charitable_Donation( $donation_id );
523
524 error_log( print_r( $donation, true ) );
525
526 /* Update the donation log. */
527 $log = new Charitable_Stripe_Donation_Log( $donation );
528
529 if ( $this->is_connect_webhook_for_connected_account() ) {
530 error_log('is_connect_webhook_for_connected_account');
531 $log->log_connected_account( $this->event->account );
532 $log->log_connect_details( $payment_intent );
533 } else {
534 error_log('NOT is_connect_webhook_for_connected_account');
535 error_log( print_r( $payment_intent->charges->data[0]->id, true ) );
536 $log->log_charge( $payment_intent->charges->data[0]->id );
537
538 /**
539 * If this was a payment on the platform but with funds going to a
540 * connected account, log the relevant details.
541 */
542 if ( ! is_null( $payment_intent->application_fee_amount ) ) {
543 $log->log_connect_details( $payment_intent );
544 }
545 }
546
547 error_log('made it to charitable-completed');
548 /* Finally, update the donation status. */
549 $donation->update_status( 'charitable-completed' );
550
551 return __( 'Donation Webhook: Donation marked as Paid', 'charitable-stripe' );
552 }
553
554 /**
555 * Process the checkout.session.completed webhook.
556 *
557 * When a session is completed, a `payment_intent.succeeded` or
558 * `payment_intent.payment_failed` event is also fired, so we
559 * update the status of the donation when that is received.
560 *
561 * However, since the payment intent is not logged when the
562 * donation is initially processed for Checkout, we record
563 * the payment intent in the Donation log here.
564 *
565 * @since 1.4.0
566 *
567 * @param object $event The Stripe event object.
568 * @return string Response message.
569 */
570 public function process_checkout_session_completed( $event ) {
571 $session = $event->data->object;
572 $donation_id = $session->client_reference_id;
573
574 /**
575 * Ensure that the session id matches the session id we recorded for this donation.
576 *
577 * @see https://bitbucket.org/wpcharitable/charitable-stripe/issues/54/webhooks-distinguish-between-webhooks-for
578 */
579 if ( $session->id != get_post_meta( $donation_id, '_stripe_session_id', true ) ) {
580 return __( 'Donation Webhook: Session id does not match donation reference on this site', 'charitable-stripe' );
581 }
582
583 /* Ensure the post type is correct. */
584 if ( Charitable::DONATION_POST_TYPE !== get_post_type( $donation_id ) ) {
585 return __( 'Donation Webhook: Donation ID not valid', 'charitable-stripe' );
586 }
587
588 /* Process subscriptions separately. */
589 if ( 'subscription' === $session->mode ) {
590 return $this->process_checkout_session_completed_for_subscription( $event );
591 }
592
593 /* If this is not a subscription, we need a payment intent. */
594 if ( is_null( $session->payment_intent ) ) {
595 return __( 'Donation Webhook: Missing payment intent', 'charitable-stripe' );
596 }
597
598 /* Mark the donation as complete. */
599 $donation = new Charitable_Donation( $donation_id );
600 $donation->update_status( 'charitable-completed' );
601
602 /* Log the Payment Intent to the session. */
603 $log = new Charitable_Stripe_Donation_Log( $donation );
604
605 if ( $this->is_connect_webhook_for_connected_account() ) {
606 $log->log_connected_account( $this->event->account );
607 $log->log_connect_details_with_payment_intent_id( $session->payment_intent, $this->get_options() );
608 } else {
609 $log->log_payment_intent( $session->payment_intent );
610 }
611
612 return __( 'Session Webhook: Donation updated with Payment Intent data', 'charitable-stripe' );
613 }
614
615 /**
616 * Process a checkout.session.completed event for a subscription.
617 *
618 * @since 1.4.3
619 *
620 * @param object $event The Stripe event object.
621 * @return string
622 */
623 public function process_checkout_session_completed_for_subscription( $event ) {
624 $session = $event->data->object;
625 $donation_id = $session->client_reference_id;
626
627 /* Make sure we have a subscription. */
628 if ( is_null( $session->subscription ) ) {
629 return __( 'Session Webhook: Missing subscription', 'charitable-stripe' );
630 }
631
632 /* Mark the donation as complete. */
633 $donation = charitable_get_donation( $donation_id );
634 $subscription = $donation->get_donation_plan();
635
636 /* Make sure a valid subscription exists. */
637 if ( ! $subscription ) {
638 return __( 'Session Webhook: Invalid subscription', 'charitable-stripe' );
639 }
640
641 /* Log the subscription id. */
642 $log = new Charitable_Stripe_Recurring_Donation_Log( $subscription );
643 $log->log_subscription( $session->subscription );
644
645 /* If the subscription should end after a certain amount of time, set that. */
646 if ( method_exists( $subscription, 'get_donation_length' ) ) {
647 $length = (int) $subscription->get_donation_length();
648
649 if ( $length ) {
650 $cancel_at = charitable_recurring_calculate_future_date(
651 $length,
652 $subscription->get_donation_period(),
653 date( 'Y-m-d 00:00:00' ),
654 'U'
655 );
656
657 /* Set the cancel_at in the subscription. */
658 try {
659 $stripe_sub = \Stripe\Subscription::retrieve( $session->subscription, $this->get_options() );
660 $stripe_sub->cancel_at = $cancel_at - HOUR_IN_SECONDS;
661 $stripe_sub->save();
662 } catch ( Exception $e ) {
663 $subscription->update_donation_log( __( 'Unable to set cancel time for subscription.', 'charitable-stripe' ) );
664 }
665 }
666 }
667
668 return __( 'Session Webhook: Donation and subscription updated with session data', 'charitable-stripe' );
669 }
670
671 /**
672 * Process the invoice.created webhook.
673 *
674 * @since 1.3.0
675 *
676 * @param object $event The Stripe event object.
677 * @return string Response message
678 */
679 public function process_invoice_created( $event ) {
680 if ( ! $this->is_recurring_installed() ) {
681 return __( 'Subscription Webhook: Unable to process without Charitable Recurring extension.', 'charitable-stripe' );
682 }
683
684 $invoice = $event->data->object;
685 $subscription = $this->get_subscription_for_webhook_object( $invoice );
686
687 if ( ! $subscription || ! is_a( $subscription, 'Charitable_Recurring_Donation' ) ) {
688 return __( 'Subscription Webhook: Missing subscription', 'charitable-stripe' );
689 }
690
691 /* Record the invoice in the subscription. */
692 if ( ! $this->is_connect_webhook_for_connected_account() ) {
693 $log = new Charitable_Stripe_Recurring_Donation_Log( $subscription );
694 $log->log_new_invoice( $invoice->id );
695 }
696
697 return __( 'Subscription Webhook: Invoice created', 'charitable-stripe' );
698 }
699
700 /**
701 * Process the invoice.payment_failed webhook.
702 *
703 * @since 1.3.0
704 *
705 * @param object $event The Stripe event object.
706 * @return string Response message
707 */
708 public function process_invoice_payment_failed( $event ) {
709 if ( ! $this->is_recurring_installed() ) {
710 return __( 'Subscription Webhook: Unable to process without Charitable Recurring extension.', 'charitable-stripe' );
711 }
712
713 $invoice = $event->data->object;
714
715 if ( ! in_array( $invoice->status, [ 'void', 'uncollectible' ] ) ) {
716 return sprintf( __( 'Subscription Webhook: Not processing invoice with a status of %s.', 'charitable-stripe' ), $invoice->status );
717 }
718
719 $subscription = $this->get_subscription_for_webhook_object( $invoice );
720
721 if ( empty( $subscription ) || ! is_a( $subscription, 'Charitable_Recurring_Donation' ) ) {
722 return __( 'Subscription Webhook: Missing subscription', 'charitable-stripe' );
723 }
724
725 $subscription_log = new Charitable_Stripe_Recurring_Donation_Log( $subscription );
726 $subscription->set_to_failed(
727 $subscription_log->get_failed_invoice_log_message( $invoice->id, $invoice->payment_intent )
728 );
729
730 return __( 'Subscription Webhook: Invoice payment failed', 'charitable-stripe' );
731 }
732
733 /**
734 * Process the invoice.payment_succeeded webhook.
735 *
736 * @since 1.3.0
737 *
738 * @param object $event The Stripe event object.
739 * @return string Response message
740 */
741 public function process_invoice_payment_succeeded( $event ) {
742 if ( ! $this->is_recurring_installed() ) {
743 return __( 'Subscription Webhook: Unable to process without Charitable Recurring extension.', 'charitable-stripe' );
744 }
745
746 $invoice = $event->data->object;
747 $subscription = $this->get_subscription_for_webhook_object( $invoice );
748
749 if ( empty( $subscription ) || ! is_a( $subscription, 'Charitable_Recurring_Donation' ) ) {
750 return __( 'Subscription Webhook: Missing subscription', 'charitable-stripe' );
751 }
752
753 /* The first donation is pending, which means this is the payment for that webhook. */
754 $first_donation = $subscription->get_first_donation_id();
755
756 if ( 'charitable-pending' == get_post_status( $first_donation ) ) {
757 $donation_id = $first_donation;
758 $donation = charitable_get_donation( $donation_id );
759 } else {
760 /* Check whether we've already added this renewal. */
761 if ( charitable_get_donation_by_transaction_id( $invoice->payment_intent ) ) {
762 return __( 'Subscription Webhook: Renewal has already been added', 'charitable-stripe' );
763 }
764
765 $donation_id = $subscription->create_renewal_donation( [ 'status' => 'charitable-completed' ] );
766 $donation = charitable_get_donation( $donation_id );
767 }
768
769 /* Update the log. */
770 $log = new Charitable_Stripe_Donation_Log( $donation );
771
772 if ( $this->is_connect_webhook_for_connected_account() ) {
773 $log->log_connected_account( $this->event->account );
774 $log->log_connect_details_with_payment_intent_id( $invoice->payment_intent, $this->get_options() );
775 } else {
776 $log->log_payment_intent( $invoice->payment_intent );
777 $log->log_charge( $invoice->charge );
778 }
779
780 /* Mark the first payment as complete. */
781 if ( $first_donation === $donation_id ) {
782 $donation->update_status( 'charitable-completed' );
783 }
784
785 /* Mark subscription as active or completed. */
786 $subscription->renew();
787
788 /* Store the donation_id in the charge's metadata to support refunds. */
789 try {
790 $charge = \Stripe\Charge::retrieve( $invoice->charge, $this->get_options() );
791 $charge->metadata = charitable_stripe_get_donation_metadata( $donation );
792 $charge->save();
793 } catch ( Exception $e ) {
794 $donation->update_donation_log( __( 'Unable to save donation ID to Stripe charge metadata.', 'charitable-stripe' ) );
795 }
796
797 return __( 'Subscription Webhook: Payment complete', 'charitable-stripe' );
798 }
799
800 /**
801 * Process the customer.subscription.updated webhook.
802 *
803 * @since 1.3.0
804 *
805 * @param object $event The Stripe event object.
806 * @return string Response message
807 */
808 public function process_customer_subscription_updated( $event ) {
809 if ( ! $this->is_recurring_installed() ) {
810 return __( 'Subscription Webhook: Unable to process without Charitable Recurring extension.', 'charitable-stripe' );
811 }
812
813 $object = $event->data->object;
814 $subscription = $this->get_subscription_for_webhook_object( $object );
815
816 if ( empty( $subscription ) ) {
817 return __( 'Subscription Webhook: Missing subscription', 'charitable-stripe' );
818 }
819
820 $stripe_status = $this->get_subscription_status( $object->status );
821 $current_status = $subscription->get_status();
822
823 if ( $stripe_status != $current_status && 'charitable-completed' != $current_status ) {
824 $subscription->update_status( $stripe_status );
825 }
826
827 return __( 'Subscription Webhook: Recurring donation updated', 'charitable-stripe' );
828 }
829
830 /**
831 * Process the customer.subscription.deleted webhook.
832 *
833 * @since 1.3.0
834 *
835 * @param object $event The Stripe event object.
836 * @return string Response message
837 */
838 public function process_customer_subscription_deleted( $event ) {
839 if ( ! $this->is_recurring_installed() ) {
840 return __( 'Subscription Webhook: Unable to process without Charitable Recurring extension.', 'charitable-stripe' );
841 }
842
843 $object = $event->data->object;
844 $subscription = $this->get_subscription_for_webhook_object( $object );
845
846 if ( empty( $subscription ) ) {
847 return __( 'Subscription Webhook: Missing subscription', 'charitable-stripe' );
848 }
849
850 if ( 'charitable-completed' != $subscription->get_status() ) {
851 $subscription->update_status( 'charitable-cancelled' );
852 }
853
854 return __( 'Subscription Webhook: Recurring donation cancelled', 'charitable-stripe' );
855 }
856
857 /**
858 * Return a recurring donation object for a particular invoice, or false if
859 * none is found.
860 *
861 * @since 1.4.0
862 *
863 * @param object $object The invoice or subscription object received from Stripe.
864 * @return Charitable_Recurring_Donation|false
865 */
866 private function get_subscription_for_webhook_object( $object ) {
867 $subscription_id = 'subscription' == $object->object ? $object->id : $object->subscription;
868
869 return charitable_recurring_get_subscription_by_gateway_id( $subscription_id, 'stripe' );
870 }
871
872 /**
873 * Given a Stripe subscription status, return the corresponding Charitable status.
874 *
875 * @since 1.4.0
876 *
877 * @param string $status Stripe subscription status.
878 * @return string
879 */
880 public function get_subscription_status( $status ) {
881 switch( $status ) {
882 case 'incomplete':
883 case 'trialing':
884 return 'charitable-pending';
885
886 case 'active':
887 return 'charitable-active';
888
889 case 'past_due':
890 return 'charitable-cancel';
891
892 case 'canceled':
893 case 'unpaid':
894 case 'incomplete_expired':
895 return 'charitable-cancelled';
896 }
897 }
898
899 /**
900 * When payment failures for a particular payment intent, update the failure count.
901 *
902 * After three failures, cancel the payment intent.
903 *
904 * @since 1.4.9
905 *
906 * @param Charitable_Abstract_Donation $donation The donation to be updated.
907 * @param string $payment_intent The payment intent id.
908 * @return void
909 */
910 public function update_payment_failure_count( Charitable_Abstract_Donation $donation, $payment_intent ) {
911 $failure_count = (int) get_post_meta( $donation->ID, '_stripe_payment_intent_failure_count', true );
912 $failure_count += 1;
913
914 /* Update the failure count. */
915 update_post_meta( $donation->ID, '_stripe_payment_intent_failure_count', $failure_count );
916
917 /**
918 * Filter the threshold number of failures after which a payment intent
919 * should be cancelled.
920 *
921 * @since 1.4.9
922 *
923 * @param int $threshold The threshold number.
924 */
925 $threshold = apply_filters( 'charitable_stripe_payment_failure_cancellation_threshold', 3 );
926
927 /* The threshold has been reached, so cancel the payment intent. */
928 if ( $threshold <= $failure_count ) {
929 $intent = new Charitable_Stripe_Payment_Intent( $payment_intent );
930 $intent->cancel();
931
932 /* Add a log message. */
933 $donation->log()->add(
934 sprintf(
935 /* translators: %d: threshold */
936 __( 'The payment intent has been cancelled after %d failed payment attempts.', 'charitable-stripe' ),
937 $threshold
938 )
939 );
940 }
941 }
942
943 /**
944 * Check whether Recurring Donations is active.
945 *
946 * @since 1.4.0
947 *
948 * @return boolean
949 */
950 private function is_recurring_installed() {
951 return class_exists( 'Charitable_Recurring' );
952 }
953
954 /**
955 * For an IPN request, get the validated incoming event object.
956 *
957 * @since 1.3.0
958 * @since 1.4.0 Returns an array instead of an object.
959 *
960 * @return false|array If valid, returns an object. Otherwise false.
961 */
962 private static function get_validated_incoming_event() {
963 $body = @file_get_contents( 'php://input' );
964 $event = json_decode( $body, true );
965
966 if ( ! is_array( $event ) || ! array_key_exists( 'id', $event ) ) {
967 return false;
968 }
969
970 return $event;
971 }
972 }
973
974 endif;
975