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 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 / abstracts / class-charitable-stripe-gateway-processor.php

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

1,442 lines 40.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Base Charitable_Stripe_Gateway_Processor_Interface instance.
4 *
5 * @package Charitable Stripe/Classes/Charitable_Stripe_Gateway_Processor
6 * @author David Bisset
7 * @copyright Copyright (c) 2021-2022, WP Charitable LLC
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_Gateway_Processor' ) ) :
19
20 /**
21 * Charitable_Stripe_Gateway_Processor
22 *
23 * @since 1.3.0
24 */
25 abstract class Charitable_Stripe_Gateway_Processor implements Charitable_Stripe_Gateway_Processor_Interface {
26
27 /** The key to use to store a customer ID. */
28 const STRIPE_CUSTOMER_ID_KEY = 'stripe_customer_id';
29
30 /** The key to use to store a customer ID. */
31 const STRIPE_CUSTOMER_ID_KEY_TEST = 'stripe_customer_id_test';
32
33 /** The key to use to store a customer ID. */
34 const STRIPE_CONNECT_CUSTOMER_ID_KEY = 'stripe_connect_customer_id';
35
36 /** The key to use to store a customer ID. */
37 const STRIPE_CONNECT_CUSTOMER_ID_KEY_TEST = 'stripe_connect_customer_id_test';
38
39 /**
40 * The donation object.
41 *
42 * @since 1.3.0
43 *
44 * @var Charitable_Donation
45 */
46 protected $donation;
47
48 /**
49 * Donation log instance for this donation.
50 *
51 * @since 1.3.0
52 *
53 * @var Charitable_Donation_Log
54 */
55 protected $donation_log;
56
57 /**
58 * The donor object.
59 *
60 * @since 1.3.0
61 *
62 * @var Charitable_Donor
63 */
64 protected $donor;
65
66 /**
67 * The donation processor object.
68 *
69 * @since 1.3.0
70 *
71 * @var Charitable_Donation_Processor
72 */
73 protected $processor;
74
75 /**
76 * The Stripe gateway model.
77 *
78 * @since 1.3.0
79 *
80 * @var Charitable_Gateway_Stripe_AM
81 */
82 protected $gateway;
83
84 /**
85 * Submitted donation values.
86 *
87 * @since 1.3.0
88 *
89 * @var array
90 */
91 protected $donation_data;
92
93 /**
94 * Options passed to Stripe with certain API requests.
95 *
96 * @since 1.3.0
97 *
98 * @var array
99 */
100 protected $options = [];
101
102 /**
103 * Connect mode.
104 *
105 * This will remain unset if Stripe Connect is not active. If
106 * it is active, this will either be `direct` or `charge_owner`.
107 *
108 * @since 1.4.0
109 *
110 * @var string
111 */
112 protected $connect_mode;
113
114 /**
115 * Application fee.
116 *
117 * This will remain unset if Stripe Connect is not active.
118 *
119 * @since 1.4.0
120 *
121 * @var string
122 */
123 protected $application_fee;
124
125 /**
126 * Destination account.
127 *
128 * This will remain unset if Stripe Connect is not active or
129 * if Connect mode is set to `direct`.
130 *
131 * @since 1.4.0
132 *
133 * @var string
134 */
135 protected $destination;
136
137 /**
138 * Customer object, always on the platform.
139 *
140 * @since 1.4.0
141 *
142 * @var Charitable_Stripe_Customer
143 */
144 protected $customer;
145
146 /**
147 * Last error encountered during customer retrieval/creation.
148 *
149 * @since 1.8.10
150 *
151 * @var string
152 */
153 protected $customer_error = '';
154
155 /**
156 * Connected account customer object.
157 *
158 * @since 1.4.0
159 *
160 * @var Charitable_Stripe_Connected_Customer
161 */
162 protected $connected_customer;
163
164 /**
165 * Set up class instance.
166 *
167 * @since 1.3.0
168 *
169 * @param int $donation_id The donation ID.
170 * @param Charitable_Donation_Processor $processor The donation processor object.
171 */
172 public function __construct( $donation_id, Charitable_Donation_Processor $processor ) {
173 $this->donation = new Charitable_Donation( $donation_id );
174 $this->donation_log = $this->donation->log();
175 $this->donor = $this->donation->get_donor();
176 $this->gateway = new Charitable_Gateway_Stripe_AM();
177 $this->processor = $processor;
178 $this->donation_data = $this->processor->get_donation_data();
179
180 $this->setup_stripe_connect();
181 }
182
183 /**
184 * Set Stripe API key.
185 *
186 * @since 1.3.0
187 *
188 * @return boolean True if the API key is set. False otherwise.
189 */
190 public function set_stripe_api_key() {
191 return $this->gateway->setup_api();
192 }
193
194 /**
195 * Return the submitted value for a gateway field.
196 *
197 * @since 1.3.0
198 *
199 * @param string $key The key of the value we want to get.
200 * @param mixed[] $values An values in which to search.
201 * @return string|false
202 */
203 public static function get_gateway_value( $key, $values ) {
204 if ( isset( $values['gateways']['stripe'][ $key ] ) ) {
205 return $values['gateways']['stripe'][ $key ];
206 }
207
208 return false;
209 }
210
211 /**
212 * Return the submitted value for a gateway field.
213 *
214 * @since 1.3.0
215 *
216 * @param string $key The key of the value we want to get.
217 * @return string|false
218 */
219 public function get_gateway_value_from_processor( $key ) {
220 return self::get_gateway_value( $key, $this->donation_data );
221 }
222
223 /**
224 * Return the statement_descriptor value.
225 *
226 * @since 1.3.0
227 *
228 * @param string|null $campaign Optional Campaign object. If not passed, will get the
229 * list of campaigns donated to from the Donation object.
230 * @return string
231 * @version 1.8.9.1
232 */
233 public function get_statement_descriptor( $campaign = null ) {
234 $format = charitable_get_option( [ 'gateways_stripe', 'statement_descriptor' ], 'auto' );
235 $descriptor = substr( charitable_get_option( [ 'gateways_stripe', 'statement_descriptor_custom' ], '' ), 0, 22 );
236
237 if ( 'auto' == $format || empty( $descriptor ) ) {
238 if ( is_null( $campaign ) ) {
239 $campaign = $this->donation->get_campaigns_donated_to();
240 }
241
242 /**
243 * Filter the automatically formatted statement_descriptor.
244 *
245 * @since 1.3.0
246 *
247 * @param string $descriptor The default descriptor.
248 * @param Charitable_Donation $donation The donation object.
249 * @param Charitable_Donation_Processor $processor The processor object.
250 */
251 $descriptor = apply_filters( 'charitable_stripe_statement_descriptor', $campaign, $this->donation, $this->processor );
252 }
253
254 /* Strip invalid characters. */
255 $descriptor = $this->strip_invalid_characters( $descriptor );
256
257 /* The descriptor must be at least 5 characters long. */
258 if ( strlen( $descriptor ) < 5 ) {
259 $url = wp_parse_url( home_url() );
260 $descriptor = $descriptor . ' ' . $url['host'];
261 }
262
263 /* Finally, ensure that the descriptor is no more than 22 characters long. */
264 return substr( $descriptor, 0, 22 );
265 }
266
267 /**
268 * Strip invalid characters from the descriptor.
269 *
270 * @since 1.4.3
271 *
272 * @param string $descriptor The descriptor.
273 * @return string
274 */
275 public function strip_invalid_characters( $descriptor ) {
276 return str_replace(
277 [
278 '<',
279 '>',
280 '\\',
281 "'",
282 '"',
283 '*',
284 ],
285 '',
286 $descriptor
287 );
288 }
289
290 /**
291 * Return the description value of the charge.
292 *
293 * @since 1.3.0
294 *
295 * @return string
296 */
297 public function get_charge_description() {
298 return html_entity_decode( $this->donation->get_campaigns_donated_to(), ENT_COMPAT, 'UTF-8' );
299 }
300
301 /**
302 * Return the charge metadata.
303 *
304 * @since 1.3.0
305 *
306 * @return array
307 */
308 public function get_charge_metadata() {
309 /**
310 * Filter the charge metadata.
311 *
312 * @since 1.3.0
313 *
314 * @param array $metadata The set of metadata.
315 * @param Charitable_Donation $donation The donation object.
316 * @param Charitable_Donation_Processor $processor The processor object.
317 */
318 return apply_filters(
319 'charitable_stripe_charge_metadata',
320 charitable_stripe_get_donation_metadata( $this->donation ),
321 $this->donation,
322 $this->processor
323 );
324 }
325
326 /**
327 * Get the donation amount in the smallest common currency unit.
328 *
329 * @since 1.3.0
330 *
331 * @param float $amount The donation amount in dollars.
332 * @param string|null $currency The currency of the donation. If null, the site currency will be used.
333 * @return int|false Returns integer if valid amount was passed, otherwise false.
334 */
335 public static function get_sanitized_donation_amount( $amount, $currency = null ) {
336 if ( is_wp_error( $amount ) ) {
337 return false;
338 }
339
340 /* Unless it's a zero decimal currency, multiply the currency x 100 to get the amount in cents. */
341 if ( self::is_zero_decimal_currency( $currency ) ) {
342 $amount = (float) $amount * 1;
343 } else {
344 $amount = (float) $amount * 100;
345 }
346
347 return absint( round( $amount ) );
348 }
349
350 /**
351 * Returns whether the currency is a zero decimal currency.
352 *
353 * @since 1.3.0
354 *
355 * @param string $currency The currency for the charge. If left blank, will check for the site currency.
356 * @return boolean
357 */
358 public static function is_zero_decimal_currency( $currency = null ) {
359 if ( is_null( $currency ) ) {
360 $currency = charitable_get_currency();
361 }
362
363 return in_array( strtoupper( $currency ), self::get_zero_decimal_currencies() );
364 }
365
366 /**
367 * Return all zero-decimal currencies supported by Stripe.
368 *
369 * @since 1.3.0
370 *
371 * @return array
372 */
373 public static function get_zero_decimal_currencies() {
374 return [
375 'BIF',
376 'CLP',
377 'DJF',
378 'GNF',
379 'JPY',
380 'KMF',
381 'KRW',
382 'MGA',
383 'PYG',
384 'RWF',
385 'VND',
386 'VUV',
387 'XAF',
388 'XOF',
389 'XPF',
390 ];
391 }
392
393 /**
394 * Returns the payment source.
395 *
396 * This may return a string, identifying the ID of a payment source such as
397 * a credit card. It may also be an associative array containing the user's
398 * credit card details.
399 *
400 * @see https://stripe.com/docs/api#create_charge
401 *
402 * @since 1.3.0
403 *
404 * @param string $customer_id Stripe customer id.
405 * @return false|string|array False if we don't have the data we need, a string if a source or token was
406 * available in the request, or an array if card data was passed in the request.
407 */
408 public function get_payment_source( $customer_id ) {
409 $source = $this->get_gateway_value_from_processor( 'source' );
410
411 if ( $source ) {
412 return $source;
413 }
414
415 $source = $this->get_gateway_value_from_processor( 'token' );
416
417 if ( ! $source ) {
418 return false;
419 }
420
421 /* Store the payment source for the Customer, and obtain a Card object from Stripe */
422 $customer = new Charitable_Stripe_Customer( $customer_id );
423 $stripe_customer = $customer->get_customer();
424
425 $card = $stripe_customer->sources->create( [ 'source' => $source ] );
426
427 return $card->id;
428 }
429
430 /**
431 * Return the Stripe Customer ID for the current customer.
432 *
433 * If the donor has donated previously through Stripe, this will return
434 * their ID from the database. If not, this will first set them up as a
435 * customer in Stripe, store their customer ID and then return it.
436 *
437 * @see https://stripe.com/docs/api#create_customer
438 *
439 * @since 1.3.0
440 *
441 * @param string|null $payment_method Optional payment method to attach to donor.
442 * @return string|false Customer id if it exists, or false otherwise.
443 */
444 public function get_stripe_customer( $payment_method = null ) {
445 $debug = charitable_is_debug( 'stripe' );
446
447 $this->customer = Charitable_Stripe_Customer::init_with_donor( $this->donor );
448
449 if ( $debug ) {
450 if ( is_null( $this->customer ) ) {
451 $user_id = $this->donor->get_user()->ID;
452 $donor_id = $this->donor->donor_id;
453 error_log( sprintf( '[Charitable Stripe] init_with_donor returned null. Donor ID: %s, User ID: %s. Will attempt to create new customer.', $donor_id, $user_id ? $user_id : '0 (guest)' ) );
454 } else {
455 error_log( sprintf( '[Charitable Stripe] init_with_donor found existing customer: %s', $this->customer->get( 'id' ) ) );
456 }
457 }
458
459 /* Check if customer object exists or has been deleted in Stripe. If needed, create one. */
460 if ( is_null( $this->customer ) || is_null( $this->customer->get( 'id' ) ) ) {
461 $this->customer = Charitable_Stripe_Customer::create_for_donor( $this->donor );
462 }
463
464 if ( is_null( $this->customer ) ) {
465 $this->customer_error = __( 'Unable to create Stripe customer. The Stripe API may be unreachable or returned an error.', 'charitable' );
466 if ( $debug ) {
467 if ( charitable_is_debug( 'stripe' ) ) {
468 error_log( '[Charitable Stripe] get_stripe_customer FAILED: create_for_donor returned null. Customer could not be created.' );
469 }
470 }
471 return false;
472 }
473
474 $customer_id = $this->customer->get( 'id' );
475
476 if ( $debug ) {
477 if ( charitable_is_debug( 'stripe' ) ) {
478 error_log( sprintf( '[Charitable Stripe] Customer ID resolved: %s', $customer_id ) );
479 }
480 }
481
482 if ( ! is_null( $payment_method ) ) {
483 if ( $debug ) {
484 if ( charitable_is_debug( 'stripe' ) ) {
485 error_log( sprintf( '[Charitable Stripe] Attaching payment method: %s to customer: %s', $payment_method, $customer_id ) );
486 }
487 }
488
489 $payment_method = $this->customer->add_payment_method( $payment_method, true );
490
491 if ( is_null( $payment_method ) ) {
492 $this->customer_error = $this->customer->get_last_error();
493 if ( $debug ) {
494 if ( charitable_is_debug( 'stripe' ) ) {
495 error_log( '[Charitable Stripe] get_stripe_customer FAILED: add_payment_method returned null.' );
496 }
497 }
498 return false;
499 }
500 }
501
502 /**
503 * When charges are made directly against different Stripe accounts, the
504 * customer needs to be added to the connected Stripe account.
505 *
506 * @see https://stripe.com/docs/connect/shared-customers
507 */
508 if ( ! empty( $this->options ) && ( 'direct' === $this->connect_mode || $this->is_recurring_donation() ) ) {
509 if ( $debug ) {
510 $account = isset( $this->options['stripe_account'] ) ? $this->options['stripe_account'] : 'unknown';
511 if ( charitable_is_debug( 'stripe' ) ) {
512 error_log( sprintf( '[Charitable Stripe] Creating connected customer on account: %s (connect_mode: %s)', $account, $this->connect_mode ) );
513 }
514 }
515
516 $this->connected_customer = new Charitable_Stripe_Connected_Customer( $this->customer, $this->options, $this->donor, $payment_method );
517 $customer_id = $this->connected_customer->get( 'id' );
518
519 if ( is_null( $customer_id ) ) {
520 $this->customer_error = __( 'Unable to create customer on connected Stripe account.', 'charitable' );
521 if ( $debug ) {
522 if ( charitable_is_debug( 'stripe' ) ) {
523 error_log( '[Charitable Stripe] get_stripe_customer FAILED: connected customer ID is null.' );
524 }
525 }
526 }
527 }
528
529 return is_null( $customer_id ) ? false : $customer_id;
530 }
531
532 /**
533 * Return a token for a shared customer.
534 *
535 * @since 1.3.0
536 *
537 * @param string $customer Stripe customer id.
538 * @return string|false
539 */
540 public function get_stripe_shared_customer_token( $customer ) {
541 try {
542 $token = \Stripe\Token::create( [ 'customer' => $customer ], $this->options );
543 } catch ( Exception $e ) {
544 $body = $e->getJsonBody();
545 $message = isset( $body['error']['message'] ) ? $body['error']['message'] : __( 'Something went wrong.', 'charitable' );
546
547 charitable_get_notices()->add_error( $message );
548
549 return false;
550 }//end try
551
552 return $token->id;
553 }
554
555 /**
556 * Return a Stripe customer id for a customer already existing
557 * on the platform account.
558 *
559 * @since 1.3.0
560 *
561 * @param string $customer_id The customer id on the platform account.
562 * @return string|false
563 */
564 public function get_connected_stripe_customer( $customer_id ) {
565 /* First, check if we already have a customer id for the customer on this account. */
566 $connected_id = $this->get_saved_connected_stripe_customer();
567
568 if ( $connected_id ) {
569 return $connected_id;
570 }
571
572 /* Get a token for the customer. */
573 $token = $this->get_stripe_shared_customer_token( $customer_id );
574
575 if ( ! $token ) {
576 return false;
577 }
578
579 try {
580 /* Retrieve the Customer object from the platform */
581 $original = new Charitable_Stripe_Customer( $customer_id );
582
583 /* Add the shared customer to the connected account, using the token above. */
584 $customer = \Stripe\Customer::create(
585 [
586 'email' => $original->get( 'email' ),
587 'description' => $original->get( 'description' ),
588 'source' => $token,
589 ],
590 $this->options
591 );
592
593 $this->save_connected_stripe_customer( $customer->id );
594
595 return $customer->id;
596
597 } catch ( Exception $e ) {
598 return false;
599 }
600 }
601
602 /**
603 * For a customer id on the platform account, check if we have already
604 * added it to the connected account.
605 *
606 * @since 1.3.0
607 *
608 * @return string|false
609 */
610 public function get_saved_connected_stripe_customer() {
611 $key = charitable_get_option( 'test_mode' ) ? self::STRIPE_CONNECT_CUSTOMER_ID_KEY_TEST : self::STRIPE_CONNECT_CUSTOMER_ID_KEY;
612 $meta = $this->donor->__get( $key );
613 $account = $this->options['stripe_account'];
614
615 if ( ! is_array( $meta ) || ! array_key_exists( $account, $meta ) ) {
616 return false;
617 }
618
619 return $meta[ $account ];
620 }
621
622 /**
623 * Save the connected account id of a customer for a given customer
624 * that already exists on the platform account.
625 *
626 * @since 1.3.0
627 *
628 * @param string $connected_id The customer id on the connected account.
629 * @return string|false
630 */
631 public function save_connected_stripe_customer( $connected_id ) {
632 $key = charitable_get_option( 'test_mode' ) ? self::STRIPE_CONNECT_CUSTOMER_ID_KEY_TEST : self::STRIPE_CONNECT_CUSTOMER_ID_KEY;
633 $meta = $this->donor->__get( $key );
634 $account = $this->options['stripe_account'];
635
636 if ( ! is_array( $meta ) ) {
637 $meta = [];
638 }
639
640 $meta[ $account ] = $connected_id;
641
642 update_user_meta( $this->donor->ID, $key, $meta );
643 }
644
645 /**
646 * Return payment intent data.
647 *
648 * @since 1.4.0
649 *
650 * @return array
651 */
652 public function get_payment_intent_data() {
653
654 $data = [
655 'description' => $this->get_charge_description(),
656 'statement_descriptor' => $this->get_statement_descriptor(),
657 'metadata' => $this->get_charge_metadata(),
658 'setup_future_usage' => $this->is_recurring_donation() ? 'off_session' : 'on_session',
659 ];
660
661 if ( isset( $this->application_fee ) && $this->application_fee > 0 ) {
662 $data['application_fee_amount'] = $this->application_fee;
663 }
664
665 if ( ! is_null( $this->destination ) && is_a( $this, 'Charitable_Stripe_Gateway_Processor_Checkout' ) ) {
666 $data['transfer_data'] = [ 'destination' => $this->destination ];
667
668 /**
669 * If desired, return true to make the connected account the settlement merchant.
670 *
671 * @since 1.4.0
672 *
673 * @param boolean $enabled Whether to make the connected account the settlement merchant.
674 */
675 if ( apply_filters( 'charitable_stripe_connect_enable_on_behalf_of', false ) ) {
676 $data['on_behalf_of'] = $this->destination;
677 }
678 }
679
680 // Filter added in 1.8.1.8.
681 $data = apply_filters( 'charitable_stripe_payment_intent_data', $data, $this );
682
683 return $data;
684 }
685
686 /**
687 * Create a recurring donation plan in Stripe.
688 *
689 * @since 1.3.0
690 *
691 * @param mixed $return The default return value.
692 * @param int $campaign_id The campaign ID.
693 * @param array $plan_args The plan parameters/arguments.
694 * @param array $args Other arguments related to the donation.
695 * @return string|false
696 */
697 public static function create_plan( $return, $campaign_id, $plan_args, $args ) {
698 $options = array_key_exists( 'options', $args ) ? $args['options'] : null;
699 $currency = charitable_get_currency();
700 $zero_decimal_currency = self::is_zero_decimal_currency( $currency );
701 $period = self::get_plan_period( $plan_args );
702 $interval = self::get_plan_interval( $plan_args );
703 $amount = self::sanitize_plan_amount( $plan_args['amount'], $currency, $zero_decimal_currency );
704 $amount_description = strval( $zero_decimal_currency ? $amount : $amount / 100 );
705 $plan_id = $period . '-' . $interval . '-' . $amount . $currency . '-' . $campaign_id;
706 $plan_name = sprintf(
707 /* translators: %1$s: campaign title; %2$s: amount; %3$s: currency; %4$s: period */
708 _x( '%1$s - %2$s %3$s every %4$s', 'campaign title — amount every period', 'charitable' ),
709 get_the_title( $campaign_id ),
710 charitable_sanitize_amount( $amount_description ),
711 $currency,
712 charitable_recurring_get_donation_periods_i18n( $interval, $period )
713 );
714
715 $product_id = self::get_stripe_campaign_product_id( $campaign_id, $options );
716
717 if ( ! $product_id ) {
718 $product_id = self::create_stripe_campaign_product( $campaign_id, $options );
719
720 if ( ! $product_id ) {
721 return false;
722 }
723 }
724
725 try {
726 $plan = \Stripe\Plan::create(
727 [
728 'id' => $plan_id,
729 'interval' => $period,
730 'interval_count' => $interval,
731 'currency' => $currency,
732 'amount' => $amount,
733 'product' => $product_id,
734 ],
735 $options
736 );
737
738 return $plan_id;
739
740 } catch ( Exception $e ) {
741
742 $response = json_decode( $e->getHttpBody() );
743
744 /* The plan already exists. */
745 if ( 'resource_already_exists' == $response->error->code ) {
746 return $plan_id;
747 }
748
749 /* Log the error message and return false. */
750 charitable_get_notices()->add_error( 'STRIPE - Error creating plan: ' . $e->getMessage() );
751
752 return false;
753
754 }//end try
755 }
756
757 /**
758 * Get a Stripe plan.
759 *
760 * If the plan already exists in the database, this will ensure
761 * that it also exists within Stripe. If it doesn't, it will create
762 * it.
763 *
764 * If the plan doesn't exist in the database, this will add it
765 * and also create it in Stripe.
766 *
767 * @since 1.3.0
768 *
769 * @param int $campaign_id The campaign ID.
770 * @param array $args Additional arguments used to define the plan.
771 *
772 * @return string The plan ID.
773 */
774 public function get_stripe_plan( $campaign_id, $args ) {
775 $plans = get_post_meta( $campaign_id, 'stripe_donation_plans', true );
776 $plan_args = charitable_recurring_get_plan_args( $args );
777 $plan_key = charitable_recurring_get_plan_key( $plan_args );
778 $mode = charitable_get_option( 'test_mode' ) ? 'test' : 'live';
779 $plan_id = false;
780
781 if ( ! is_array( $plans ) ) {
782 $plans = [];
783 }
784
785 /**
786 * Check whether the plan has been created before, and if it has,
787 * make sure it still exists in Stripe.
788 */
789 if ( isset( $plans[ $mode ][ $plan_key ] ) ) {
790 $options = array_key_exists( 'options', $args ) ? $args['options'] : null;
791 $plan_id = $this->stripe_plan_exists( $plans[ $mode ][ $plan_key ], $options );
792 }
793
794 if ( ! $plan_id ) {
795 /**
796 * Create the Stripe plan.
797 *
798 * This filter replicates the filter defined in `charitable_recurring_campaign_create_gateway_plan_id`.
799 * As of Charitable Stripe 1.3, that function is not utilized by the Stripe extension since its approach
800 * is not quite ideal. Hence, we apply the filter here.
801 *
802 * @since 1.3.0
803 *
804 * @param boolean|string $return The plan ID. Set to false by default.
805 * @param int $campaign_id The campaign ID.
806 * @param array $plan_args The plan arguments.
807 * @param array $args Additional arguments.
808 */
809 $plan_id = apply_filters( 'charitable_recurring_create_gateway_plan_' . Charitable_Gateway_Stripe_AM::get_gateway_id(), false, $campaign_id, $plan_args, $args );
810
811 if ( $plan_id ) {
812 $plans[ $mode ][ $plan_key ] = $plan_id;
813
814 update_post_meta( $campaign_id, 'stripe_donation_plans', $plans );
815 }
816 }
817
818 return $plan_id;
819 }
820
821 /**
822 * Verify that the plan exists.
823 *
824 * @since 1.3.0
825 *
826 * @param string $plan_id The plan ID.
827 * @param array|null $options Options to pass to Stripe.
828 * @return false|string The plan ID if it exists. False otherwise.
829 */
830 public function stripe_plan_exists( $plan_id, $options = null ) {
831 try {
832 $plan = \Stripe\Plan::retrieve( $plan_id, $options );
833
834 return $plan_id;
835
836 } catch ( Exception $e ) {
837 return false;
838 }
839 }
840
841 /**
842 * Get the Stripe product ID for a campaign, creating the product if necessary.
843 *
844 * @since 1.3.0
845 *
846 * @param int $campaign_id The campaign ID.
847 * @param array|null $options Options to pass to Stripe.
848 * @return string|false
849 */
850 public static function get_stripe_campaign_product_id( $campaign_id, $options = null ) {
851 $product_id = get_post_meta( $campaign_id, 'stripe_product_id', true );
852
853 if ( ! $product_id ) {
854 return false;
855 }
856
857 /* The product may have been deleted within Stripe, so make sure we can retrieve it. */
858 try {
859 $product = \Stripe\Product::retrieve( $product_id, $options );
860
861 return $product_id;
862
863 } catch ( Exception $e ) {
864 return false;
865 }
866 }
867
868 /**
869 * Create a product in Stripe for the campaign.
870 *
871 * @since 1.3.0
872 *
873 * @param int $campaign_id The campaign ID.
874 * @param array|null $options Options to pass to Stripe.
875 * @return string|false
876 * @version 1.8.9.1
877 */
878 public static function create_stripe_campaign_product( $campaign_id, $options = null ) {
879 /* No product could be retrieved, so we need to create one. */
880 $statement_descriptor = $this->get_statement_descriptor( get_the_title( $campaign_id ) );
881
882 try {
883 $product = \Stripe\Product::create(
884 [
885 'name' => get_the_title( $campaign_id ),
886 'type' => 'service',
887 'statement_descriptor' => $statement_descriptor,
888 'metadata' => [
889 'campaign_id' => $campaign_id,
890 ],
891 ],
892 $options
893 );
894
895 update_post_meta( $campaign_id, 'stripe_product_id', $product->id );
896
897 return $product->id;
898
899 } catch ( Exception $e ) {
900 /* Log the error message and return false. */
901 if ( charitable_is_debug() ) {
902 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
903 error_log( 'STRIPE - Error creating product: ' . $e->getMessage() );
904 }
905
906 return false;
907 }
908 }
909
910 /**
911 * Given a Stripe subscription, return the Charitable subscription status.
912 *
913 * @since 1.3.0
914 *
915 * @param object $subscription The subscription object from Stripe.
916 * @return string
917 */
918 public static function get_subscription_status( $subscription ) {
919 switch ( $subscription->status ) {
920 case 'active':
921 $status = 'charitable-active';
922 break;
923
924 case 'past_due':
925 case 'canceled':
926 case 'unpaid':
927 $status = 'charitable-failed';
928 break;
929
930 default:
931 $status = 'charitable-pending';
932 }
933
934 /**
935 * Filter the status for a recurring subscription based on a Stripe status.
936 *
937 * @since 1.2.0
938 *
939 * @param string $status The Charitable status.
940 * @param string $stripe_status Stripe's status for the subscription.
941 */
942 return apply_filters( 'charitable_stripe_recurring_subscription_status', $status, $subscription->status );
943 }
944
945 /**
946 * Return the Stripe period given a set of plan args.
947 *
948 * @since 1.3.0
949 *
950 * @param array $args The plan args.
951 * @return string
952 */
953 public static function get_plan_period( $args ) {
954 switch ( $args['period'] ) {
955 case 'month':
956 case 'quarter':
957 case 'semiannual':
958 $period = 'month';
959 break;
960
961 default:
962 $period = $args['period'];
963 }
964
965 return $period;
966 }
967
968 /**
969 * Return the Stripe billing interval, given a set of plan args.
970 *
971 * @since 1.3.0
972 *
973 * @param array $args The plan args.
974 * @return int
975 */
976 public static function get_plan_interval( $args ) {
977 switch ( $args['period'] ) {
978 case 'quarter':
979 $interval = 3;
980 break;
981
982 case 'semiannual':
983 $interval = 6;
984 break;
985
986 default:
987 $interval = $args['interval'];
988 }
989
990 return $interval;
991 }
992
993 /**
994 * Sanitize the plan amount.
995 *
996 * @since 1.3.0
997 *
998 * @param string $amount The plan amount.
999 * @param string $currency The site currency.
1000 * @param boolean $zero_decimal_currency Whether the site is using a zero decimal currency.
1001 * @return string
1002 */
1003 public static function sanitize_plan_amount( $amount, $currency, $zero_decimal_currency ) {
1004 if ( version_compare( charitable_recurring()->get_version(), '1.0.5', '<' ) ) {
1005 return $zero_decimal_currency ? $amount / 100 : $amount;
1006 } else {
1007 return self::get_sanitized_donation_amount( $amount, $currency );
1008 }
1009 }
1010
1011 /**
1012 * Checks whether the donation being processed is recurring.
1013 *
1014 * @since 1.4.0
1015 *
1016 * @return boolean
1017 */
1018 public function is_recurring_donation() {
1019 return false !== $this->processor->get_donation_data_value( 'donation_plan', false );
1020 }
1021
1022 /**
1023 * Set the $charges property to empty.
1024 *
1025 * @since 1.3.0
1026 *
1027 * @return void
1028 */
1029 public function clear_charges() {
1030 $this->charges = [];
1031 }
1032
1033 /**
1034 * Return the results of all charges.
1035 *
1036 * @since 1.3.0
1037 *
1038 * @return array
1039 */
1040 public function get_charges() {
1041 return $this->charges;
1042 }
1043
1044 /**
1045 * Saves the results of a charge.
1046 *
1047 * @since 1.3.0
1048 *
1049 * @param mixed $result The result of a Stripe charge.
1050 * @param string $status The status of the charge.
1051 * @return void
1052 */
1053 public function save_charge_results( $result, $status ) {
1054 $this->charges[] = [
1055 'result' => $result,
1056 'status' => $status,
1057 ];
1058 }
1059
1060 /**
1061 * When a charge fails and raises an exception, save the result and
1062 * add a notice for the error.
1063 *
1064 * @since 1.3.0
1065 *
1066 * @param Exception $e Exception thrown.
1067 * @param string $message Fallback message to be logged if one isn't set in the exception body.
1068 * @return void
1069 */
1070 public function save_charge_error( Exception $e, $message ) {
1071 $body = $e->getJsonBody();
1072
1073 if ( isset( $body['error']['message'] ) ) {
1074 $message = $body['error']['message'];
1075 }
1076
1077 charitable_get_notices()->add_error( $message );
1078
1079 $this->save_charge_results( $body, 'error' );
1080 }
1081
1082 /**
1083 * Log a failed Stripe charge.
1084 *
1085 * @since 1.3.0
1086 *
1087 * @param array $charge_result The charge result.
1088 * @return void
1089 */
1090 public function log_error( $charge_result ) {
1091 $this->donation_log->add(
1092 sprintf(
1093 /* translators: %s: type of error */
1094 __( 'Stripe error: %s', 'charitable' ),
1095 '<code>' . $charge_result['result']['error']['type'] . '</code>'
1096 )
1097 );
1098 }
1099
1100 /**
1101 * Log a successful Stripe charge.
1102 *
1103 * @since 1.3.0
1104 *
1105 * @param array $charge_result The charge result.
1106 * @return void
1107 */
1108 public function log_success( $charge_result ) {
1109 /* Charge includes an application fee. */
1110 if ( ! is_null( $charge_result->application_fee ) ) {
1111 $this->log_application_fee( $charge_result );
1112 }
1113
1114 /* Charge is on our account (not directly on a connected account). */
1115 if ( is_null( $charge_result->application ) || ! is_null( $charge_result->destination ) ) {
1116 $url = sprintf(
1117 'https://dashboard.stripe.com/%spayments/%s',
1118 $charge_result->livemode ? '' : 'test/',
1119 $charge_result->id
1120 );
1121
1122 $this->donation_log->add(
1123 sprintf(
1124 /* translators: %s: link to Stripe charge details */
1125 __( 'Stripe charge: %s', 'charitable' ),
1126 '<a href="' . $url . '" target="_blank"><code>' . $charge_result->id . '</code></a>'
1127 )
1128 );
1129 }
1130 }
1131
1132 /**
1133 * Log an application fee for a charge.
1134 *
1135 * @since 1.3.0
1136 *
1137 * @param array $charge_result The charge result.
1138 * @return void
1139 */
1140 public function log_application_fee( $charge_result ) {
1141 $url = sprintf(
1142 'https://dashboard.stripe.com/%sapplications/fees/%s',
1143 $charge_result->livemode ? '' : 'test/',
1144 $charge_result->application_fee
1145 );
1146
1147 $this->donation_log->add(
1148 sprintf(
1149 /* translators: %s: link to Stripe application fee details */
1150 __( 'Stripe application fee: %s', 'charitable' ),
1151 '<a href="' . $url . '" target="_blank"><code>' . $charge_result->application_fee . '</code></a>'
1152 )
1153 );
1154 }
1155
1156 /**
1157 * Set up connected account data for this donation.
1158 *
1159 * @since 1.4.0
1160 * @version 1.8.9.1
1161 *
1162 * @return void
1163 */
1164 public function setup_stripe_connect() {
1165 $debug = charitable_is_debug( 'stripe' );
1166
1167 $campaign_donations = $this->donation->get_campaign_donations();
1168
1169 if ( charitable_is_debug() ) {
1170 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
1171 error_log( 'setup_stripe_connect in embedded stripe' );
1172 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
1173 error_log( print_r( $campaign_donations, true ) );
1174 }
1175
1176 if ( $debug ) {
1177 $campaign_id = current( $campaign_donations )->campaign_id;
1178 error_log( sprintf( '[Charitable Stripe] setup_stripe_connect called. Campaign ID: %s, Using Stripe Connect: %s', $campaign_id, charitable_using_stripe_connect() ? 'yes' : 'no' ) ); // phpcs:ignore
1179 }
1180
1181 $this->application_fee = $this->get_application_fee_amount(
1182 current( $campaign_donations )->amount,
1183 0
1184 );
1185
1186 if ( ! class_exists( 'Charitable_Stripe_Connect' ) ) {
1187 if ( $debug ) {
1188 if ( charitable_is_debug( 'stripe' ) ) {
1189 error_log( '[Charitable Stripe] Legacy Charitable_Stripe_Connect class not found. Using built-in Stripe Connect handling.' ); // phpcs:ignore
1190 }
1191 }
1192 }
1193
1194 $campaign_donations = $this->donation->get_campaign_donations();
1195
1196 /* We cannot support multiple campaign donations in a single donation with Stripe Connect. */
1197 if ( 1 < count( $campaign_donations ) ) {
1198 wp_die( esc_html( __( 'Error: Unable to process multiple campaign donations in a single donation with Stripe Connect.', 'charitable' ) ) );
1199 }
1200
1201 $connected_account = charitable_stripe_get_connected_account_for_campaign( current( $campaign_donations )->campaign_id );
1202
1203 if ( ! $connected_account ) {
1204 if ( $debug ) {
1205 if ( charitable_is_debug( 'stripe' ) ) {
1206 error_log( sprintf( '[Charitable Stripe] No legacy connected account for campaign %s. connect_mode: %s, options: %s', current( $campaign_donations )->campaign_id, $this->connect_mode ? $this->connect_mode : 'not set', ! empty( $this->options ) ? wp_json_encode( $this->options ) : 'empty' ) ); // phpcs:ignore
1207 }
1208 }
1209 return;
1210 }
1211
1212 $this->connect_mode = charitable_get_option( [ 'gateways_stripe', 'charge_owner' ] );
1213
1214 /* Recurring donations on connected accounts are processed directly on the connected account. */
1215 if ( 'direct' == $this->connect_mode || $this->is_recurring_donation() ) {
1216 $this->options['stripe_account'] = $connected_account;
1217 } else {
1218 $this->destination = $connected_account;
1219 }
1220
1221 if ( $debug ) {
1222 if ( charitable_is_debug( 'stripe' ) ) {
1223 error_log( sprintf( '[Charitable Stripe] setup_stripe_connect completed. connect_mode: %s, connected_account: %s, destination: %s', $this->connect_mode, $connected_account, $this->destination ? $this->destination : 'none' ) ); // phpcs:ignore
1224 }
1225 }
1226
1227 $this->application_fee = $this->get_application_fee_amount(
1228 current( $campaign_donations )->amount,
1229 charitable_get_option( [ 'gateways_stripe', 'application_fee' ] )
1230 );
1231 }
1232
1233 /**
1234 * Returns whether the current payment will be processed directly on a connected account.
1235 *
1236 * @since 1.4.0
1237 *
1238 * @return boolean
1239 */
1240 public function using_connected_account() {
1241 return is_array( $this->options ) && array_key_exists( 'stripe_account', $this->options );
1242 }
1243
1244 /**
1245 * Get the application fee percentage to apply.
1246 *
1247 * @since 1.4.3
1248 *
1249 * @return false|int
1250 */
1251 public function get_application_fee_percentage() {
1252 if ( ! $this->connect_mode ) {
1253 return false;
1254 }
1255
1256 return charitable_get_option( [ 'gateways_stripe', 'application_fee' ], false );
1257 }
1258
1259 /**
1260 * Return the application fee to be charged for a particular amount.
1261 *
1262 * @since 1.4.2
1263 * @version 1.8.0
1264 * @version 1.8.8.3
1265 * @version 1.8.9.1
1266 *
1267 * @param mixed $amount The donation amount.
1268 * @param mixed $application_fee The application fee.
1269 * @return int
1270 */
1271 public function get_application_fee_amount( $amount, $application_fee ) {
1272
1273 $fee = 0;
1274 $amount = str_replace( ',', '.', $amount );
1275
1276 if ( charitable_is_debug() ) {
1277 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1278 error_log( 'get_application_fee_amount' );
1279 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
1280 error_log( print_r( $amount, true ) );
1281 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
1282 error_log( print_r( $application_fee, true ) );
1283 }
1284
1285 if ( round( (float) $amount ) === 0 ) {
1286 return 0;
1287 }
1288
1289 if ( charitable_is_pro() || ! charitable_using_stripe_connect() ) {
1290 $fee = round(
1291 $amount * 0,
1292 0
1293 );
1294
1295 } else {
1296 // Check if this is a zero-decimal currency (JPY, KRW, VND, etc.)
1297 if ( self::is_zero_decimal_currency() ) {
1298 // For zero-decimal currencies, use 0.03 (3% as decimal)
1299 $fee = round(
1300 (float) $amount * 0.03,
1301 0
1302 );
1303 } else {
1304 // For regular currencies (USD, EUR, GBP, etc.), use 3 (cents per dollar)
1305 $application_fee = 3;
1306 $fee = round(
1307 (float) $amount * $application_fee,
1308 0
1309 );
1310 }
1311
1312 }
1313
1314 if ( charitable_is_debug() ) {
1315 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1316 error_log( 'get_application_fee_amount fee' );
1317 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
1318 error_log( print_r( $fee, true ) );
1319 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
1320 error_log( print_r( $amount, true ) );
1321 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
1322 error_log( print_r( $application_fee, true ) );
1323 }
1324
1325 return $fee;
1326 }
1327
1328 /**
1329 * Create a Stripe Customer object through the API.
1330 *
1331 * @deprecated To be removed in 1.6.0 or 2.1.0.
1332 *
1333 * @since 1.3.0
1334 * @since 1.4.0 Deprecated.
1335 *
1336 * @return string|false
1337 */
1338 public function create_stripe_customer() {
1339 charitable_stripe_get_deprecated()->deprecated_function( __METHOD__, '1.4.0' );
1340
1341 $customer = Charitable_Stripe_Customer::create_for_donor( $this->donor );
1342
1343 if ( is_null( $customer ) || is_null( $customer->get( 'id' ) ) ) {
1344 return false;
1345 }
1346
1347 return $customer->get( 'id' );
1348 }
1349
1350 /**
1351 * Return the saved Stripe customer id from the user meta table.
1352 *
1353 * @deprecated To be removed in 1.6.0 or 2.1.0.
1354 *
1355 * @since 1.3.0
1356 * @since 1.4.0 Deprecated.
1357 *
1358 * @return string|false String if one is set, otherwise false.
1359 */
1360 public function get_saved_stripe_customer_id() {
1361 charitable_stripe_get_deprecated()->deprecated_function( __METHOD__, '1.4.0' );
1362
1363 $customer = Charitable_Stripe_Customer::init_with_donor( $this->donor );
1364
1365 if ( is_null( $customer ) || is_null( $customer->get( 'id' ) ) ) {
1366 return false;
1367 }
1368
1369 return $customer->get( 'id' );
1370 }
1371
1372 /**
1373 * Save the Stripe customer id for logged in users.
1374 *
1375 * @deprecated To be removed in 1.6.0 or 2.1.0.
1376 *
1377 * @since 1.3.0
1378 * @since 1.4.0 Deprecated.
1379 *
1380 * @param string $stripe_customer_id The Stripe customer id.
1381 * @return void
1382 */
1383 public function save_stripe_customer_id( $stripe_customer_id ) {
1384 charitable_stripe_get_deprecated()->deprecated_function( __METHOD__, '1.4.0' );
1385
1386 $key = charitable_get_option( 'test_mode' ) ? self::STRIPE_CUSTOMER_ID_KEY_TEST : self::STRIPE_CUSTOMER_ID_KEY;
1387
1388 update_user_meta( $this->donor->ID, $key, $stripe_customer_id );
1389 }
1390
1391 /**
1392 * Return the Stripe Customer object for a particular Stripe customer id.
1393 *
1394 * @deprecated To be removed in 1.6.0 or 2.1.0.
1395 *
1396 * @since 1.3.0
1397 * @since 1.4.0 Deprecated.
1398 *
1399 * @param string $stripe_customer_id The Stripe customer id.
1400 * @return object|null
1401 */
1402 public function get_stripe_customer_object( $stripe_customer_id ) {
1403 charitable_stripe_get_deprecated()->deprecated_function( __METHOD__, '1.4.0' );
1404
1405 $customer = new Charitable_Stripe_Customer( $stripe_customer_id );
1406
1407 return $customer->get_customer();
1408 }
1409
1410 /**
1411 * Returns a card ID for the customer.
1412 *
1413 * @deprecated To be removed in 1.6.0 or 2.1.0.
1414 *
1415 * @since 1.3.0
1416 * @since 1.4.0 Deprecated.
1417 *
1418 * @param string $customer Stripe's customer ID.
1419 * @param string $card_args The customer's card details or token.
1420 * @return string|false Card ID or false if Stripe returns an error.
1421 */
1422 public function get_stripe_customer_card_id( $customer, $card_args ) {
1423 charitable_stripe_get_deprecated()->deprecated_function( __METHOD__, '1.4.0' );
1424
1425 try {
1426 $customer = $this->get_stripe_customer_object( $customer );
1427 $card = $customer->sources->create( [ 'source' => $card_args ] );
1428 } catch ( Exception $e ) {
1429 $body = $e->getJsonBody();
1430 $message = isset( $body['error']['message'] ) ? $body['error']['message'] : __( 'Something went wrong.', 'charitable' );
1431
1432 charitable_get_notices()->add_error( $message );
1433
1434 return false;
1435 }
1436
1437 return $card->id;
1438 }
1439 }
1440
1441 endif;
1442