PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.7.0.11
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.7.0.11
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.11, at includes/stripe/gateway/class-charitable-stripe-webhook-processor.php

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