PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.4.1
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.4.1
2.4.1 2.4.0 2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.2 2.2.1 2.2.0 2.1.2 2.1.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 All 68 releases
← All changes | includes/Classes/PaymentRequestGuard.php +115 -198 2.3.4 → 2.4.1 View file →
@@ -32,8 +32,16 @@
32 32 * Every payment entry point must call it before talking to a gateway: the widget handlers
33 33 * (`Classes\Actions`), the block handlers (`Blocks\BlockActions`) and the Elementor Pro
34 34 * form actions. `tests/Unit/Classes/PaymentRequestGuardWiringTest.php` fails if one stops.
35 35 *
36 + * Better Payment Pro's payment forms — Payment Form layouts 4/5/6 and the recurring,
37 + * split-payment and mixed payment types — are not resolved by the rules in this class.
38 + * PaymentEntitlement refuses them outright without Pro and otherwise asks Pro (or, for
39 + * an older Pro, Lite's transitional copy of Pro's code) to resolve them. Those resolvers use
40 + * the public checks below (resolve_amount(), coupon_code(), stripe_price(),
41 + * product_price(), …), so a Pro form is held to exactly the same amount, price and coupon
42 + * rules as a free one.
43 + *
36 44 * @since 2.3.4
37 45 */
38 46 class PaymentRequestGuard {
39 47
@@ -67,10 +75,17 @@
67 75 return $this->helper;
68 76 }
69 77
70 78 /**
71 - * Resolve a widget or block submission (Pro layouts 4/5/6 included).
79 + * Resolve a widget or block submission.
72 80 *
81 + * A Pro payment form (a Pro layout, PaymentEntitlement::is_pro_form()) is refused on a free
82 + * install before anything else is looked up, and otherwise resolved through
83 + * PaymentEntitlement::resolve_form_payment() — which fails closed when nothing resolves it.
84 + * A request posting a field only a Pro form renders is refused on a free install too, and
85 + * on every install when its form is not a Pro layout. A free layout is a one-time payment,
86 + * whatever payment type its saved settings still hold.
87 + *
73 88 * @param array $settings Stored widget/block settings — never request data.
74 89 * @param array $fields Submitted fields: the AJAX `fields` array, or `$_POST` for PayPal. Unslashed.
75 90 * @param array $context `page_id` (int) of the form, `gateway` ('stripe'|'paypal'|'paystack').
76 91 *
@@ -77,24 +92,36 @@
77 92 * @return array|WP_Error {
78 93 * @type float $amount Total to charge (unit amount × quantity).
79 94 * @type float $unit_amount Validated amount per unit.
80 95 * @type int|string $quantity Submitted quantity, or '' when none was sent.
81 - * @type int[] $product_quantities Layout 6 per-product quantities, keyed like the product id setting.
96 + * @type int[] $product_quantities Per-product quantities of a product-list form, keyed like its product id setting.
82 97 * @type string $currency Currency to charge in.
83 98 * @type string $campaign_id Verified campaign id, or '' (never an unverified request value).
84 99 * @type string $coupon_code Coupon, only when the form has a visible coupon field.
85 - * @type bool $is_recurring Whether this is a Stripe subscription checkout.
86 - * @type string $recurring_price_id Allowed recurring price id.
87 - * @type string $installment_price_id Allowed split-payment installment price id.
100 + * @type bool $is_recurring Whether this is a Stripe subscription checkout (Pro forms only).
101 + * @type string $recurring_price_id Allowed recurring price id (Pro forms only).
102 + * @type string $installment_price_id Allowed split-payment installment price id (Pro forms only).
88 103 * }
89 104 */
90 105 public function resolve_form_payment( array $settings, array $fields, array $context ) {
91 - $page_id = isset( $context['page_id'] ) ? absint( $context['page_id'] ) : 0;
92 - $gateway = isset( $context['gateway'] ) ? sanitize_key( $context['gateway'] ) : '';
93 - $layout = self::setting( $settings, 'better_payment_form_layout', 'layout-1' );
94 - $payment_type = self::setting( $settings, 'better_payment_form_payment_type', 'one-time' );
95 - $campaign = $this->resolve_campaign( self::field( $fields, 'campaign_id' ), $page_id );
106 + $page_id = isset( $context['page_id'] ) ? absint( $context['page_id'] ) : 0;
107 + $gateway = isset( $context['gateway'] ) ? sanitize_key( $context['gateway'] ) : '';
108 + $is_pro_form = PaymentEntitlement::is_pro_form( $settings );
109 + $posts_pro_field = PaymentEntitlement::posts_pro_fields( $fields );
96 110
111 + // Before anything else is read: a free install runs none of a Pro payment.
112 + if ( ( $is_pro_form || $posts_pro_field ) && ! ProCompatibility::pro_enabled() ) {
113 + return PaymentEntitlement::refusal();
114 + }
115 +
116 + // A free layout never renders a plan, an installment or a mixed choice, whatever payment
117 + // type its saved settings still hold. Such a request is forged, and never reaches Pro.
118 + if ( ! $is_pro_form && $posts_pro_field ) {
119 + return self::invalid_price();
120 + }
121 +
122 + $campaign = $this->resolve_campaign( self::field( $fields, 'campaign_id' ), $page_id );
123 +
97 124 $resolved = [
98 125 'amount' => 0.0,
99 126 'unit_amount' => 0.0,
100 127 'quantity' => '',
@@ -100,26 +127,38 @@
100 127 'quantity' => '',
101 128 'product_quantities' => [],
102 129 'currency' => null !== $campaign && '' !== $campaign['currency'] ? $campaign['currency'] : self::form_currency( $settings ),
103 130 'campaign_id' => null !== $campaign ? $campaign['id'] : '',
104 - 'coupon_code' => self::allowed_coupon_code( $settings, $layout, $fields ),
131 + 'coupon_code' => '',
105 132 'is_recurring' => false,
106 133 'recurring_price_id' => '',
107 134 'installment_price_id' => '',
108 135 ];
109 136
110 - if ( in_array( $payment_type, [ 'recurring', 'split-payment' ], true ) ) {
111 - return $this->resolve_subscription( $resolved, $settings, $fields, $payment_type, $gateway );
137 + if ( $is_pro_form ) {
138 + return PaymentEntitlement::resolve_form_payment(
139 + $this,
140 + $resolved,
141 + $settings,
142 + $fields,
143 + [
144 + 'page_id' => $page_id,
145 + 'gateway' => $gateway,
146 + 'campaign' => $campaign,
147 + ]
148 + );
112 149 }
113 150
114 - // A subscription checkout is only ever rendered for a recurring/split form.
115 - if ( 'subscription' === self::field( $fields, 'better_payment_recurring_mode' ) ) {
116 - return self::invalid_price();
117 - }
151 + $form_fields = self::repeater( $settings, 'better_payment_form_fields' );
152 + $resolved['coupon_code'] = self::coupon_code( $form_fields, $fields );
118 153
119 - $amount = 'layout-6-pro' === $layout
120 - ? $this->resolve_product_list_amount( $settings, $fields )
121 - : $this->resolve_single_amount( $settings, $fields, $layout, $campaign );
154 + $amount = $this->resolve_amount(
155 + $settings,
156 + $fields,
157 + $form_fields,
158 + self::preset_amounts( $settings, 'better_payment_show_amount_list', 'better_payment_amount' ),
159 + $campaign
160 + );
122 161
123 162 if ( is_wp_error( $amount ) ) {
124 163 return $amount;
125 164 }
@@ -246,17 +285,21 @@
246 285
247 286 // ------------------------------------------------------------------ one-time amounts
248 287
249 288 /**
250 - * Layouts 1–5: a product-priced form, a verified campaign donation, or the amount field's rules.
289 + * A one-time amount: a product-priced form, a verified campaign donation, or the amount field's rules.
251 290 *
252 - * @param array $settings Form settings.
253 - * @param array $fields Submitted fields.
254 - * @param string $layout Form layout.
255 - * @param array|null $campaign Verified campaign.
256 - * @return array|WP_Error
291 + * Every one-time form goes through this, Pro's included — Pro's resolver passes the
292 + * fields repeater and preset amounts its layout renders.
293 + *
294 + * @param array $settings Form settings.
295 + * @param array $fields Submitted fields.
296 + * @param array $form_fields The fields repeater the form renders.
297 + * @param float[] $presets Preset amounts the form renders as radios (preset_amounts()).
298 + * @param array|null $campaign Verified campaign (resolve_campaign()), or null.
299 + * @return array|WP_Error `amount`, `unit_amount`, `quantity`, `product_quantities`.
257 300 */
258 - private function resolve_single_amount( array $settings, array $fields, $layout, $campaign ) {
301 + public function resolve_amount( array $settings, array $fields, array $form_fields, array $presets, $campaign = null ) {
259 302 $posted = self::posted_amount( $fields, 'primary_payment_amount' );
260 303 $quantity = self::posted_quantity( $fields );
261 304
262 305 if ( is_wp_error( $quantity ) ) {
@@ -283,9 +326,9 @@
283 326 } elseif ( null !== $campaign ) {
284 327 if ( ! self::campaign_allows( $campaign, $posted ) ) {
285 328 return self::invalid_amount();
286 329 }
287 - } elseif ( ! self::amount_field_allows( $settings, $layout, $posted ) ) {
330 + } elseif ( ! self::amount_field_allows( $form_fields, $presets, $posted ) ) {
288 331 return self::invalid_amount();
289 332 }
290 333
291 334 if ( $posted <= 0 ) {
@@ -300,74 +343,8 @@
300 343 ];
301 344 }
302 345
303 346 /**
304 - * Layout 6: the total of the configured WooCommerce/FluentCart products at their real prices.
305 - *
306 - * Quantities are the visitor's to choose, prices are not. Stripe and Paystack post one
307 - * quantity per product; PayPal's plain form POST keeps only the last `payment_amount_quantity`
308 - * input, so with several products and a single quantity the total can only be held to its
309 - * floor — every product at least once — which is still never less than the listed price.
310 - *
311 - * @param array $settings Form settings.
312 - * @param array $fields Submitted fields.
313 - * @return array|WP_Error
314 - */
315 - private function resolve_product_list_amount( array $settings, array $fields ) {
316 - $platform = 'fluentcart' === self::setting( $settings, 'better_payment_form_layout_6_ecommerce_platform', '' ) ? 'fluentcart' : 'woocommerce';
317 - $product_ids = isset( $settings[ "better_payment_form_{$platform}_product_ids" ] ) ? (array) $settings[ "better_payment_form_{$platform}_product_ids" ] : [];
318 - $product_ids = array_filter( array_map( 'absint', $product_ids ) );
319 -
320 - if ( empty( $product_ids ) ) {
321 - return self::price_unavailable();
322 - }
323 -
324 - $posted = self::posted_amount( $fields, 'primary_payment_amount' );
325 - $raw_quantity = isset( $fields['payment_amount_quantity'] ) ? $fields['payment_amount_quantity'] : null;
326 - $expected = 0.0;
327 - $exact = true;
328 - $quantities = [];
329 -
330 - foreach ( $product_ids as $key => $product_id ) {
331 - if ( is_array( $raw_quantity ) ) {
332 - $quantity = isset( $raw_quantity[ $key ] ) ? intval( $raw_quantity[ $key ] ) : 1;
333 - } elseif ( null !== $raw_quantity && '' !== $raw_quantity && 1 === count( $product_ids ) ) {
334 - $quantity = intval( $raw_quantity );
335 - } else {
336 - $quantity = 1;
337 - $exact = 1 === count( $product_ids );
338 - }
339 -
340 - if ( $quantity < 1 ) {
341 - return self::invalid_quantity();
342 - }
343 -
344 - $price = $this->product_price( $platform, $product_id );
345 - if ( null === $price ) {
346 - return self::price_unavailable();
347 - }
348 -
349 - $expected += $price * $quantity;
350 - $quantities[ $key ] = $quantity;
351 - }
352 -
353 - $allowed = $exact ? self::same_amount( $posted, $expected ) : $posted + self::TOLERANCE >= $expected;
354 -
355 - if ( $expected <= 0 || ! $allowed ) {
356 - return self::invalid_amount();
357 - }
358 -
359 - $amount = $exact ? $expected : $posted;
360 -
361 - return [
362 - 'amount' => $amount,
363 - 'unit_amount' => $amount,
364 - 'quantity' => 1,
365 - 'product_quantities' => $quantities,
366 - ];
367 - }
368 -
369 - /**
370 347 * Whether an amount is one the form's amount field could have submitted.
371 348 *
372 349 * Mirrors partials/layout-repeater-vars.php and the layouts that include it:
373 350 * - a preset amount (radio) is always allowed — clicking one writes it into the input,
@@ -375,20 +352,20 @@
375 352 * - a dynamic amount comes from the URL, so only min/max apply;
376 353 * - a fixed or hidden field can only submit its configured default;
377 354 * - otherwise the visitor types it, within min (default 1, as rendered) and max.
378 355 *
379 - * @param array $settings Form settings.
380 - * @param string $layout Form layout.
381 - * @param float $amount Submitted amount.
356 + * @param array $form_fields The fields repeater the form renders.
357 + * @param float[] $presets Preset amounts the form renders.
358 + * @param float $amount Submitted amount.
382 359 * @return bool
383 360 */
384 - private static function amount_field_allows( array $settings, $layout, $amount ) {
385 - if ( self::amount_in( $amount, self::preset_amounts( $settings, $layout ) ) ) {
361 + private static function amount_field_allows( array $form_fields, array $presets, $amount ) {
362 + if ( self::amount_in( $amount, $presets ) ) {
386 363 return true;
387 364 }
388 365
389 366 $field = null;
390 - foreach ( self::form_fields( $settings, $layout ) as $item ) {
367 + foreach ( $form_fields as $item ) {
391 368 if ( is_array( $item ) && 'primary_payment_amount' === ( isset( $item['better_payment_primary_field_type'] ) ? $item['better_payment_primary_field_type'] : '' ) ) {
392 369 $field = $item;
393 370 break;
394 371 }
@@ -486,71 +463,8 @@
486 463
487 464 return false;
488 465 }
489 466
490 - // ------------------------------------------------------------------ subscriptions
491 -
492 - /**
493 - * Recurring and split-payment forms: the price ids must be ones the form offers.
494 - *
495 - * The price decides what Stripe charges, so a request-supplied price id is as dangerous
496 - * as a request-supplied amount — any price in the merchant's Stripe account would do.
497 - *
498 - * @param array $resolved Base resolution.
499 - * @param array $settings Form settings.
500 - * @param array $fields Submitted fields.
501 - * @param string $payment_type 'recurring' | 'split-payment'.
502 - * @param string $gateway Gateway handling the request.
503 - * @return array|WP_Error
504 - */
505 - private function resolve_subscription( array $resolved, array $settings, array $fields, $payment_type, $gateway ) {
506 - // Recurring/split layouts render Stripe only; any other gateway would charge a
507 - // one-time amount of the visitor's choosing instead of the plan.
508 - if ( 'stripe' !== $gateway ) {
509 - return new WP_Error( 'bp_subscription_gateway', __( 'This payment plan can only be paid with Stripe.', 'better-payment' ) );
510 - }
511 -
512 - $is_split = 'split-payment' === $payment_type;
513 - $main_price = self::setting( $settings, 'better_payment_form_recurring_price_id', '' );
514 - $list_key = $is_split ? 'better_payment_split_installment_price_ids' : 'better_payment_recurring_price_ids';
515 - $item_key = $is_split ? 'better_payment_split_installment_price_id' : 'better_payment_recurring_price_id';
516 - $allowed = [ $main_price ];
517 -
518 - foreach ( (array) ( isset( $settings[ $list_key ] ) ? $settings[ $list_key ] : [] ) as $item ) {
519 - if ( is_array( $item ) && ! empty( $item[ $item_key ] ) ) {
520 - $allowed[] = sanitize_text_field( $item[ $item_key ] );
521 - }
522 - }
523 -
524 - $allowed = array_values( array_filter( $allowed ) );
525 - $price_id = self::field( $fields, 'better_payment_recurring_price_id' );
526 - $price_id = '' === $price_id ? $main_price : $price_id;
527 -
528 - if ( '' === $price_id || ! in_array( $price_id, $allowed, true ) ) {
529 - return self::invalid_price();
530 - }
531 -
532 - $installment_id = $is_split ? self::field( $fields, 'split_payment_installment' ) : '';
533 - if ( '' !== $installment_id && ! in_array( $installment_id, $allowed, true ) ) {
534 - return self::invalid_price();
535 - }
536 -
537 - $charged_price = '' !== $installment_id ? $installment_id : $price_id;
538 - $unit_amount = $this->stripe_price_amount( $charged_price, self::stripe_secret_key( $settings ) );
539 -
540 - if ( null === $unit_amount ) {
541 - return self::price_unavailable();
542 - }
543 -
544 - $resolved['amount'] = $unit_amount;
545 - $resolved['unit_amount'] = $unit_amount;
546 - $resolved['is_recurring'] = true;
547 - $resolved['recurring_price_id'] = $price_id;
548 - $resolved['installment_price_id'] = $installment_id;
549 -
550 - return $resolved;
551 - }
552 -
553 467 // ------------------------------------------------------------------ campaigns
554 468
555 469 /**
556 470 * A Campaign Builder campaign (`bp_campaign` post).
@@ -735,8 +649,22 @@
735 649 return $product_id ? $this->product_price( $source, $product_id ) : null;
736 650 }
737 651
738 652 /**
653 + * Unit amount (major units) of a Stripe price, read with the form's own Stripe secret key.
654 + *
655 + * Looked up once per request. Null when it cannot be read — refuse the payment then; it
656 + * is never charged at the visitor's amount.
657 + *
658 + * @param mixed $price_id Stripe price id; anything but a scalar reads as none.
659 + * @param array $settings Form settings (they select the live or test secret key).
660 + * @return float|null
661 + */
662 + public function stripe_price( $price_id, array $settings ) {
663 + return $this->stripe_price_amount( is_scalar( $price_id ) ? (string) $price_id : '', self::stripe_secret_key( $settings ) );
664 + }
665 +
666 + /**
739 667 * Current price of a WooCommerce or FluentCart product, read the way the form renders it.
740 668 *
741 669 * @param string $platform 'woocommerce' | 'fluentcart'.
742 670 * @param int $product_id Product id.
@@ -741,9 +669,9 @@
741 669 * @param string $platform 'woocommerce' | 'fluentcart'.
742 670 * @param int $product_id Product id.
743 671 * @return float|null
744 672 */
745 - private function product_price( $platform, $product_id ) {
673 + public function product_price( $platform, $product_id ) {
746 674 $price = null;
747 675
748 676 if ( 'woocommerce' === $platform && function_exists( 'wc_get_product' ) ) {
749 677 $product = wc_get_product( $product_id );
@@ -820,43 +748,33 @@
820 748
821 749 // ------------------------------------------------------------------ form configuration
822 750
823 751 /**
824 - * The fields repeater a layout renders — same mapping as Actions::fetch_better_form_fields().
752 + * A repeater setting (a list of items), or [] when it is unset or not a list.
825 753 *
826 754 * @param array $settings Form settings.
827 - * @param string $layout Form layout.
755 + * @param string $key Setting key, e.g. `better_payment_form_fields`.
828 756 * @return array
829 757 */
830 - private static function form_fields( array $settings, $layout ) {
831 - $keys = [
832 - 'layout-4-pro' => 'better_payment_form_fields_layout_4_5_6',
833 - 'layout-5-pro' => 'better_payment_form_fields_layout_4_5_6_desc',
834 - 'layout-6-pro' => 'better_payment_form_fields_layout_4_5_6_woo',
835 - ];
836 - $key = isset( $keys[ $layout ] ) ? $keys[ $layout ] : 'better_payment_form_fields';
837 -
758 + public static function repeater( array $settings, $key ) {
838 759 return isset( $settings[ $key ] ) && is_array( $settings[ $key ] ) ? $settings[ $key ] : [];
839 760 }
840 761
841 762 /**
842 - * Preset amounts the layout renders as radios.
763 + * Preset amounts a form renders as radios: the items of `$items_key`, while `$toggle_key` is on.
843 764 *
844 - * @param array $settings Form settings.
845 - * @param string $layout Form layout.
765 + * @param array $settings Form settings.
766 + * @param string $toggle_key Show-amount-list switch, e.g. `better_payment_show_amount_list`.
767 + * @param string $items_key Amount list repeater, e.g. `better_payment_amount`.
846 768 * @return float[]
847 769 */
848 - private static function preset_amounts( array $settings, $layout ) {
849 - $is_pro_layout = in_array( $layout, [ 'layout-4-pro', 'layout-5-pro', 'layout-6-pro' ], true );
850 - $toggle = $is_pro_layout ? 'better_payment_show_amount_list_layout_4_5_6' : 'better_payment_show_amount_list';
851 - $items = $is_pro_layout ? 'better_payment_amount_layout_4_5_6' : 'better_payment_amount';
852 -
853 - if ( 'yes' !== self::setting( $settings, $toggle, '' ) || empty( $settings[ $items ] ) || ! is_array( $settings[ $items ] ) ) {
770 + public static function preset_amounts( array $settings, $toggle_key, $items_key ) {
771 + if ( 'yes' !== self::setting( $settings, $toggle_key, '' ) || empty( $settings[ $items_key ] ) || ! is_array( $settings[ $items_key ] ) ) {
854 772 return [];
855 773 }
856 774
857 775 $amounts = [];
858 - foreach ( $settings[ $items ] as $item ) {
776 + foreach ( $settings[ $items_key ] as $item ) {
859 777 if ( is_array( $item ) && isset( $item['better_payment_amount_val'] ) ) {
860 778 $amounts[] = floatval( $item['better_payment_amount_val'] );
861 779 }
862 780 }
@@ -866,14 +784,13 @@
866 784
867 785 /**
868 786 * A coupon code, only when the form actually offers a visible coupon field.
869 787 *
870 - * @param array $settings Form settings.
871 - * @param string $layout Form layout.
872 - * @param array $fields Submitted fields.
788 + * @param array $form_fields The fields repeater the form renders.
789 + * @param array $fields Submitted fields.
873 790 * @return string
874 791 */
875 - private static function allowed_coupon_code( array $settings, $layout, array $fields ) {
792 + public static function coupon_code( array $form_fields, array $fields ) {
876 793 $code = self::field( $fields, 'primary_coupon_code' );
877 794
878 795 if ( '' === $code ) {
879 796 return '';
@@ -878,9 +795,9 @@
878 795 if ( '' === $code ) {
879 796 return '';
880 797 }
881 798
882 - foreach ( self::form_fields( $settings, $layout ) as $item ) {
799 + foreach ( $form_fields as $item ) {
883 800 if ( is_array( $item )
884 801 && 'primary_coupon_code' === ( isset( $item['better_payment_primary_field_type'] ) ? $item['better_payment_primary_field_type'] : '' )
885 802 && self::is_visible( $item ) ) {
886 803 return $code;
@@ -902,9 +819,9 @@
902 819 * metadata `better_payment_public = yes`.
903 820 * Anything else refuses the checkout rather than silently charging full price for a code
904 821 * the donor believes applied.
905 822 *
906 - * Call this only with a code allowed_coupon_code() has let through.
823 + * Call this only with a code coupon_code() has let through.
907 824 *
908 825 * @since 2.3.4
909 826 *
910 827 * @param string $code Coupon code from resolve_form_payment().
@@ -1014,9 +931,9 @@
1014 931 * @param array $fields Submitted fields.
1015 932 * @param string $key Amount input name.
1016 933 * @return float
1017 934 */
1018 - private static function posted_amount( array $fields, $key ) {
935 + public static function posted_amount( array $fields, $key ) {
1019 936 $amount = self::field( $fields, $key );
1020 937
1021 938 if ( '' === $amount || 0.0 === floatval( $amount ) ) {
1022 939 $amount = self::field( $fields, 'primary_payment_amount_radio' );
@@ -1051,9 +968,9 @@
1051 968 * @param array $fields Submitted fields.
1052 969 * @param string $key Field name.
1053 970 * @return string
1054 971 */
1055 - private static function field( array $fields, $key ) {
972 + public static function field( array $fields, $key ) {
1056 973 if ( ! isset( $fields[ $key ] ) ) {
1057 974 return '';
1058 975 }
1059 976
@@ -1072,9 +989,9 @@
1072 989 * @param string $key Key.
1073 990 * @param string $fallback Value when unset or empty.
1074 991 * @return string
1075 992 */
1076 - private static function setting( array $settings, $key, $fallback ) {
993 + public static function setting( array $settings, $key, $fallback ) {
1077 994 return isset( $settings[ $key ] ) && is_scalar( $settings[ $key ] ) && '' !== (string) $settings[ $key ]
1078 995 ? sanitize_text_field( (string) $settings[ $key ] )
1079 996 : $fallback;
1080 997 }
@@ -1136,9 +1053,9 @@
1136 1053 * @param float $a Amount.
1137 1054 * @param float $b Amount.
1138 1055 * @return bool
1139 1056 */
1140 - private static function same_amount( $a, $b ) {
1057 + public static function same_amount( $a, $b ) {
1141 1058 return abs( (float) $a - (float) $b ) < self::TOLERANCE;
1142 1059 }
1143 1060
1144 1061 /**
@@ -1143,9 +1060,9 @@
1143 1060
1144 1061 /**
1145 1062 * @return WP_Error
1146 1063 */
1147 - private static function invalid_amount() {
1064 + public static function invalid_amount() {
1148 1065 return new WP_Error( 'bp_invalid_amount', __( 'The payment amount is not allowed for this form.', 'better-payment' ) );
1149 1066 }
1150 1067
1151 1068 /**
@@ -1150,9 +1067,9 @@
1150 1067
1151 1068 /**
1152 1069 * @return WP_Error
1153 1070 */
1154 - private static function invalid_quantity() {
1071 + public static function invalid_quantity() {
1155 1072 return new WP_Error( 'bp_invalid_quantity', __( 'The quantity must be a whole number of at least 1.', 'better-payment' ) );
1156 1073 }
1157 1074
1158 1075 /**
@@ -1157,9 +1074,9 @@
1157 1074
1158 1075 /**
1159 1076 * @return WP_Error
1160 1077 */
1161 - private static function invalid_price() {
1078 + public static function invalid_price() {
1162 1079 return new WP_Error( 'bp_invalid_price', __( 'The selected payment plan is not available for this form.', 'better-payment' ) );
1163 1080 }
1164 1081
1165 1082 /**
@@ -1178,8 +1095,8 @@
1178 1095
1179 1096 /**
1180 1097 * @return WP_Error
1181 1098 */
1182 - private static function price_unavailable() {
1099 + public static function price_unavailable() {
1183 1100 return new WP_Error( 'bp_price_unavailable', __( 'The price for this payment could not be verified. Please try again later.', 'better-payment' ) );
1184 1101 }
1185 1102 }