| @@ -12,10 +12,20 @@ | ||
| 12 | 12 | class Scalapay extends PayplugGenericGateway |
| 13 | 13 | { |
| 14 | 14 | protected $allowed_country_codes = []; |
| 15 | 15 | protected $enable_refund = true; |
| 16 | - const ENABLE_ON_TEST_MODE = false; | |
| 16 | + public const ENABLE_ON_TEST_MODE = false; | |
| 17 | 17 | |
| 18 | + /** | |
| 19 | + * Last-resort fallback bounds, in cents. Used only when neither the account's live API | |
| 20 | + * amounts nor the stored scalapay.default_amounts are available - e.g. an options array | |
| 21 | + * that predates that key, since Configuration loads the raw stored options without | |
| 22 | + * merging the schema defaults. Mirrors Configuration::$expected_fields' | |
| 23 | + * payment_methods.configuration.scalapay.default_amounts. | |
| 24 | + */ | |
| 25 | + public const DEFAULT_MIN_AMOUNT = 500; | |
| 26 | + public const DEFAULT_MAX_AMOUNT = 400000; | |
| 27 | + | |
| 18 | 28 | public function __construct() |
| 19 | 29 | { |
| 20 | 30 | parent::__construct(); |
| 21 | 31 | |
| @@ -36,8 +46,123 @@ | ||
| 36 | 46 | $this->enabled = 'no'; |
| 37 | 47 | } |
| 38 | 48 | |
| 39 | 49 | add_action('woocommerce_order_item_add_action_buttons', [$this, 'refund_not_available']); |
| 50 | + add_action('woocommerce_after_checkout_validation', [$this, 'validate_checkout'], 10); | |
| 51 | + } | |
| 52 | + | |
| 53 | + /** | |
| 54 | + * Server-side guard mirroring check_gateway(): a stale/bypassed client could still submit | |
| 55 | + * an order outside the merchant's configured (or account-authorized) amount range. | |
| 56 | + * | |
| 57 | + * @throws \Exception | |
| 58 | + */ | |
| 59 | + public function validate_checkout(): void | |
| 60 | + { | |
| 61 | + $posted_data = $this->get_post_data(); | |
| 62 | + | |
| 63 | + if (($posted_data['payment_method'] ?? '') !== $this->id) { | |
| 64 | + return; | |
| 65 | + } | |
| 66 | + | |
| 67 | + [$min_cents, $max_cents] = self::effective_bounds( | |
| 68 | + $this->settings['payment_methods']['configuration']['scalapay'] ?? [], | |
| 69 | + $this->settings['payment_methods']['permissions']['scalapay']['amounts'] ?? '{}' | |
| 70 | + ); | |
| 71 | + | |
| 72 | + // Compared in euros rather than multiplying the total by 100: a float cents value | |
| 73 | + // such as 19.99 * 100 => 1998.9999999999998 can fall the wrong side of an exact | |
| 74 | + // boundary, whereas dividing the (integer) bound by 100 yields the same double as | |
| 75 | + // the parsed total. Same direction as PayplugGatewayOney3x's amount checks. | |
| 76 | + $order_total = (float) $this->get_order_total(); | |
| 77 | + if ($order_total < $min_cents / 100 || $order_total > $max_cents / 100) { | |
| 78 | + throw new \Exception(sprintf(__('The total amount of your order should be between %s€ and %s€ to pay with Scalapay.', 'payplug'), $min_cents / 100, $max_cents / 100)); | |
| 79 | + } | |
| 80 | + } | |
| 81 | + | |
| 82 | + /** | |
| 83 | + * Remove Scalapay from the available gateways if the order/cart amount is outside the | |
| 84 | + * merchant's configured bounds (or the account's authorized range, if unconfigured). | |
| 85 | + * | |
| 86 | + * @param array $gateways | |
| 87 | + * | |
| 88 | + * @return array | |
| 89 | + */ | |
| 90 | + public function check_gateway($gateways) | |
| 91 | + { | |
| 92 | + // WC()->cart is only populated on frontend requests (WooCommerce::is_request('frontend') | |
| 93 | + // excludes wp-admin and REST requests) but this filter also fires outside that context - | |
| 94 | + // e.g. the WC Settings > Payments screen calling get_available_payment_gateways() via | |
| 95 | + // PaymentsController::store_has_enabled_gateways(). get_order_total() dereferences | |
| 96 | + // WC()->cart->total unconditionally, which would throw a PHP warning on a null cart. | |
| 97 | + if (isset($gateways[$this->id]) && $gateways[$this->id]->id == $this->id && null !== WC()->cart) { | |
| 98 | + [$min_cents, $max_cents] = self::effective_bounds( | |
| 99 | + $this->settings['payment_methods']['configuration']['scalapay'] ?? [], | |
| 100 | + $this->settings['payment_methods']['permissions']['scalapay']['amounts'] ?? '{}' | |
| 101 | + ); | |
| 102 | + | |
| 103 | + // Euro comparison, for the float-precision reason spelled out in validate_checkout(). | |
| 104 | + $order_total = (float) $this->get_order_total(); | |
| 105 | + if ($order_total < $min_cents / 100 || $order_total > $max_cents / 100) { | |
| 106 | + unset($gateways[$this->id]); | |
| 107 | + } | |
| 108 | + } | |
| 109 | + | |
| 110 | + return parent::check_gateway($gateways); | |
| 111 | + } | |
| 112 | + | |
| 113 | + /** | |
| 114 | + * The account's PayPlug-authorized range, in cents: the live API amounts when available, | |
| 115 | + * otherwise the stored default_amounts fallback. This is the ceiling the merchant may | |
| 116 | + * only narrow, never widen - shared with the BO display (PaymentMethods.php) and the | |
| 117 | + * save-time validator's bounds (Ajax.php) so the three can't silently drift apart. | |
| 118 | + * | |
| 119 | + * @param array $config payment_methods.configuration.scalapay (default_amounts) | |
| 120 | + * @param string $api_amounts payment_methods.permissions.scalapay.amounts, JSON-encoded | |
| 121 | + * {"min":{"EUR":n},"max":{"EUR":n}} | |
| 122 | + * | |
| 123 | + * @return array{0: int, 1: int} [min_cents, max_cents] | |
| 124 | + */ | |
| 125 | + public static function authorized_bounds($config, $api_amounts) | |
| 126 | + { | |
| 127 | + $authorized = json_decode($api_amounts ?? '{}', true); | |
| 128 | + $min = is_array($authorized) ? ($authorized['min']['EUR'] ?? null) : null; | |
| 129 | + $max = is_array($authorized) ? ($authorized['max']['EUR'] ?? null) : null; | |
| 130 | + | |
| 131 | + if (null === $min || null === $max) { | |
| 132 | + $fallback = json_decode($config['default_amounts'] ?? '', true); | |
| 133 | + $fallback = is_array($fallback) ? $fallback : []; | |
| 134 | + $min = $min ?? ($fallback['min'] ?? self::DEFAULT_MIN_AMOUNT); | |
| 135 | + $max = $max ?? ($fallback['max'] ?? self::DEFAULT_MAX_AMOUNT); | |
| 136 | + } | |
| 137 | + | |
| 138 | + return [(int) $min, (int) $max]; | |
| 139 | + } | |
| 140 | + | |
| 141 | + /** | |
| 142 | + * The merchant's configured min/max (when set) narrowed against the account's live | |
| 143 | + * PayPlug-authorized range - shared with the BO settings display (PaymentMethods.php) | |
| 144 | + * so both stay in sync by construction. | |
| 145 | + * | |
| 146 | + * @param array $config payment_methods.configuration.scalapay (custom_amounts/default_amounts) | |
| 147 | + * @param string $api_amounts payment_methods.permissions.scalapay.amounts, JSON-encoded | |
| 148 | + * {"min":{"EUR":n},"max":{"EUR":n}} | |
| 149 | + * | |
| 150 | + * @return array{0: int, 1: int} [min_cents, max_cents] | |
| 151 | + */ | |
| 152 | + public static function effective_bounds($config, $api_amounts) | |
| 153 | + { | |
| 154 | + $custom_amounts = json_decode($config['custom_amounts'] ?? '{}', true); | |
| 155 | + $custom_amounts = is_array($custom_amounts) ? $custom_amounts : []; | |
| 156 | + | |
| 157 | + [$authorized_min, $authorized_max] = self::authorized_bounds($config, $api_amounts); | |
| 158 | + | |
| 159 | + // !empty() rather than isset(): a stored 0 is not a meaningful Scalapay threshold, | |
| 160 | + // so it falls back to the account bound instead of being read as an override. | |
| 161 | + $min = !empty($custom_amounts['min']) ? (int) $custom_amounts['min'] : $authorized_min; | |
| 162 | + $max = !empty($custom_amounts['max']) ? (int) $custom_amounts['max'] : $authorized_max; | |
| 163 | + | |
| 164 | + return [$min, $max]; | |
| 40 | 165 | } |
| 41 | 166 | |
| 42 | 167 | /** |
| 43 | 168 | * @param \WC_Order $order |