| @@ -2,8 +2,9 @@ | ||
| 2 | 2 | |
| 3 | 3 | namespace Better_Payment\Lite\Classes; |
| 4 | 4 | |
| 5 | 5 | use Better_Payment\Lite\Controller; |
| 6 | +use Better_Payment\Lite\Traits\Helper as TraitsHelper; | |
| 6 | 7 | |
| 7 | 8 | /** |
| 8 | 9 | * Exit if accessed directly |
| 9 | 10 | */ |
| @@ -17,188 +18,290 @@ | ||
| 17 | 18 | * get_option and update_option. |
| 18 | 19 | */ |
| 19 | 20 | class Helper extends Controller |
| 20 | 21 | { |
| 22 | + use TraitsHelper; | |
| 23 | + | |
| 21 | 24 | /** |
| 22 | - * Check if elementor plugin is activated | |
| 25 | + * The statuses of a confirmed payment, as campaign totals, donor counts and Pro's Donors Wall | |
| 26 | + * count them. See approved_donation_sql() for the one other row that counts. | |
| 23 | 27 | * |
| 24 | - * @since 0.0.2 | |
| 28 | + * `success` is the status Paystack verification writes. It was missing, so Paystack donations | |
| 29 | + * never counted toward a campaign's total or donor count, even though the transactions screen | |
| 30 | + * and analytics already treated it as paid. Only a transaction Paystack reported as succeeded | |
| 31 | + * is ever stored as `success`. | |
| 32 | + * | |
| 33 | + * @since 2.4.0 (was Campaign\CampaignStats::approved_statuses()) | |
| 25 | 34 | */ |
| 26 | - public function elementor_not_loaded() | |
| 27 | - { | |
| 28 | - if (!current_user_can('activate_plugins')) { | |
| 29 | - return; | |
| 30 | - } | |
| 35 | + const APPROVED_DONATION_STATUSES = array( 'Completed', 'paid', 'complete', 'succeeded', 'success' ); | |
| 31 | 36 | |
| 32 | - if (isset($_GET['page']) && $_GET['page'] == 'better-payment-setup-wizard') { | |
| 33 | - return; | |
| 34 | - } | |
| 37 | + /** | |
| 38 | + * The status rows written before 2.4.0 can carry for a free Stripe checkout (a 100%-off | |
| 39 | + * promotion code, a subscription trial). | |
| 40 | + * | |
| 41 | + * Such a row is one of two things, and the status alone cannot tell them apart: | |
| 42 | + * - a CONFIRMED free checkout. The old confirmation stored Stripe's `payment_status` as the | |
| 43 | + * row status and, because `amount_total` was 0 and `! empty()` treated 0 as missing, kept | |
| 44 | + * the form's full amount. Nothing was charged. | |
| 45 | + * - a checkout Stripe reported as `no_payment_required` when it was CREATED and that nobody | |
| 46 | + * completed. The old confirmation only claimed `unpaid` rows, so it was never touched. | |
| 47 | + * 2.4.0 stores neither: a confirmed row is always `paid` at its real amount, and a pending | |
| 48 | + * one `unpaid`. Stored rows are left as they are and counted by the rule below. | |
| 49 | + * | |
| 50 | + * @since 2.4.0 | |
| 51 | + */ | |
| 52 | + const LEGACY_FREE_CHECKOUT_STATUS = 'no_payment_required'; | |
| 35 | 53 | |
| 36 | - if(wp_doing_ajax()){ | |
| 37 | - return; | |
| 38 | - } | |
| 54 | + /** | |
| 55 | + * What only the old Stripe confirmation wrote: the serialized `exact_paid_amount` key of | |
| 56 | + * `form_fields_info`, followed by its float value. | |
| 57 | + * | |
| 58 | + * Every confirmation from the release that introduced discounts (and with them the only way | |
| 59 | + * to reach a free checkout) onwards set `$form_fields_info['exact_paid_amount']` to a float | |
| 60 | + * in the same UPDATE that wrote the status. No creation path ever writes that key. The | |
| 61 | + * columns the confirmation also wrote are no use: `customer_info` and `transaction_id` are | |
| 62 | + * already set at creation (the created session and its PaymentIntent), and `email` comes from | |
| 63 | + * the charge's billing details, which a checkout that charged nothing does not have. | |
| 64 | + * | |
| 65 | + * The trailing `d:` makes it the key: after a string VALUE the next token is a key (`s:` or | |
| 66 | + * `i:`) or `}`, so a form field whose value happens to be "exact_paid_amount" cannot match. | |
| 67 | + * | |
| 68 | + * @since 2.4.0 | |
| 69 | + */ | |
| 70 | + const LEGACY_CONFIRMATION_MARKER = 's:17:"exact_paid_amount";d:'; | |
| 39 | 71 | |
| 40 | - $elementor = 'elementor/elementor.php'; | |
| 72 | + /** | |
| 73 | + * SQL condition: true only for an unconfirmed legacy free checkout. Null-safe: a NULL status or | |
| 74 | + * NULL form_fields_info never makes it true. | |
| 75 | + * | |
| 76 | + * OR it into the `incomplete` bucket's `status IN (…)` clause, so the Incomplete tab lists the | |
| 77 | + * rows the Completed tab drops (DB::get_transactions()). Contains no `%`. | |
| 78 | + * | |
| 79 | + * @since 2.4.0 | |
| 80 | + */ | |
| 81 | + const UNCONFIRMED_LEGACY_SQL = "( COALESCE(status, '') = '" . self::LEGACY_FREE_CHECKOUT_STATUS . "' AND INSTR(COALESCE(form_fields_info, ''), '" . self::LEGACY_CONFIRMATION_MARKER . "') = 0 )"; | |
| 41 | 82 | |
| 42 | - if ($this->is_plugin_installed($elementor)) { | |
| 43 | - $activation_url = wp_nonce_url('plugins.php?action=activate&plugin=' . $elementor . '&plugin_status=all&paged=1&s', 'activate-plugin_' . $elementor); | |
| 83 | + /** | |
| 84 | + * SQL condition: false only for an unconfirmed legacy free checkout, the exact negation of | |
| 85 | + * UNCONFIRMED_LEGACY_SQL. Null-safe, so a row with a NULL status (PayPal's pending sentinel) or | |
| 86 | + * NULL form_fields_info is never dropped by it. | |
| 87 | + * | |
| 88 | + * AND it onto any aggregation that selects rows by a status bucket containing | |
| 89 | + * `no_payment_required` (`completed`, `all`). Admin\DB and Pro's AnalyticsModel do; Pro keeps | |
| 90 | + * the literal value, and AnalyticsModelParityTest compares the two values. | |
| 91 | + * | |
| 92 | + * @since 2.4.0 | |
| 93 | + */ | |
| 94 | + const COUNTED_ROW_SQL = 'NOT ' . self::UNCONFIRMED_LEGACY_SQL; | |
| 44 | 95 | |
| 45 | - $message = sprintf(__('%1$sBetter Payment%2$s requires %1$sElementor%2$s plugin to be active. Please activate Elementor to continue.', 'better-payment'), "<strong>", "</strong>"); | |
| 96 | + /** | |
| 97 | + * SQL expression: what a row contributes to a money total. A legacy free checkout adds 0, | |
| 98 | + * confirmed or not, because its stored amount (the form's) was never charged. | |
| 99 | + * | |
| 100 | + * @since 2.4.0 | |
| 101 | + */ | |
| 102 | + const COUNTABLE_AMOUNT_SQL = "CASE WHEN status = '" . self::LEGACY_FREE_CHECKOUT_STATUS . "' THEN 0 ELSE amount END"; | |
| 46 | 103 | |
| 47 | - $button_text = __('Activate Elementor', 'better-payment'); | |
| 48 | - } else { | |
| 49 | - $activation_url = wp_nonce_url(self_admin_url('update.php?action=install-plugin&plugin=elementor'), 'install-plugin_elementor'); | |
| 104 | + /** | |
| 105 | + * SQL condition: the row is a confirmed donation. | |
| 106 | + * | |
| 107 | + * An approved status, or a CONFIRMED legacy free checkout (see LEGACY_CONFIRMATION_MARKER). | |
| 108 | + * Pair it with COUNTABLE_AMOUNT_SQL for any sum. Contains no `%`, so it can be interpolated | |
| 109 | + * into a string passed to `$wpdb->prepare()`. | |
| 110 | + * | |
| 111 | + * Pro keeps an identical copy in Models\AnalyticsModel (it supports a Lite older than this | |
| 112 | + * method), pinned by Pro's AnalyticsModelParityTest. | |
| 113 | + * | |
| 114 | + * @since 2.4.0 | |
| 115 | + * | |
| 116 | + * @return string | |
| 117 | + */ | |
| 118 | + public static function approved_donation_sql(): string { | |
| 119 | + return "( status IN ('" . implode( "', '", self::APPROVED_DONATION_STATUSES ) . "')" | |
| 120 | + . " OR ( status = '" . self::LEGACY_FREE_CHECKOUT_STATUS . "' AND INSTR(COALESCE(form_fields_info, ''), '" . self::LEGACY_CONFIRMATION_MARKER . "') > 0 ) )"; | |
| 121 | + } | |
| 50 | 122 | |
| 51 | - $message = sprintf(__('%1$sBetter Payment%2$s requires %1$sElementor%2$s plugin to be installed and activated. Please install Elementor to continue.', 'better-payment'), '<strong>', '</strong>'); | |
| 52 | - $button_text = __('Install Elementor', 'better-payment'); | |
| 53 | - } | |
| 54 | - | |
| 55 | - $button = '<p><a href="' . esc_url_raw($activation_url) . '" class="button-primary">' . $button_text . '</a></p>'; | |
| 56 | - | |
| 57 | - ob_start(); | |
| 58 | - printf('<div class="error"><p>%1$s</p>%2$s</div>', wp_kses( __( $message, 'better-payment' ), $this->bp_allowed_tags() ), wp_kses( $button, $this->bp_allowed_tags() ) ); | |
| 59 | - ob_end_flush(); | |
| 123 | + /** | |
| 124 | + * PHP twin of `NOT COUNTED_ROW_SQL`, for rows already loaded: is this a legacy free checkout | |
| 125 | + * that was never confirmed? | |
| 126 | + * | |
| 127 | + * A row without `form_fields_info` loaded is treated as unconfirmed (never counted), the safe | |
| 128 | + * side of the rule. An already-unserialized `form_fields_info` array is read as the string it was | |
| 129 | + * stored as, so a caller that unserialized the row first gets the same answer. | |
| 130 | + * | |
| 131 | + * @since 2.4.0 | |
| 132 | + * | |
| 133 | + * @param object|array $row A transaction row. | |
| 134 | + * @return bool | |
| 135 | + */ | |
| 136 | + public static function is_unconfirmed_legacy_free_checkout( $row ): bool { | |
| 137 | + return self::LEGACY_FREE_CHECKOUT_STATUS === self::row_value( $row, 'status' ) | |
| 138 | + && false === strpos( self::form_fields_info_string( $row ), self::LEGACY_CONFIRMATION_MARKER ); | |
| 60 | 139 | } |
| 61 | 140 | |
| 62 | 141 | /** |
| 63 | - * Check if a plugin is installed | |
| 142 | + * The status bucket a loaded transaction row is labelled and listed under. | |
| 64 | 143 | * |
| 65 | - * @since 0.0.2 | |
| 144 | + * The raw status's bucket (get_type_by_transaction_status()), except for an unconfirmed legacy | |
| 145 | + * free checkout: no count or total includes it, so it is `incomplete` (v1: `unpaid`) rather than | |
| 146 | + * the `completed` its `no_payment_required` status maps to. A confirmed one stays `completed`. | |
| 147 | + * The row itself is never changed. Every PHP surface that labels a row reads this: the admin REST | |
| 148 | + * rows (`status_type`), the CSV export, UserAPI, the User Dashboard template and the block's | |
| 149 | + * editor rows. DB::get_transactions() applies the same rule to the Completed / Incomplete tabs. | |
| 150 | + * | |
| 151 | + * @since 2.4.0 | |
| 152 | + * | |
| 153 | + * @param object|array $row A transaction row. | |
| 154 | + * @param string $version 'v2' (completed / incomplete / refunded) or 'v1'. | |
| 155 | + * @return string The bucket key; '' for a status no bucket lists, as get_type_by_transaction_status(). | |
| 66 | 156 | */ |
| 67 | - public function is_plugin_installed($basename) | |
| 68 | - { | |
| 69 | - if (!function_exists('get_plugins')) { | |
| 70 | - include_once ABSPATH . 'wp-admin/includes/plugin.php'; | |
| 157 | + public static function transaction_status_type( $row, string $version = 'v2' ): string { | |
| 158 | + if ( self::is_unconfirmed_legacy_free_checkout( $row ) ) { | |
| 159 | + return 'v1' === $version ? 'unpaid' : 'incomplete'; | |
| 71 | 160 | } |
| 72 | 161 | |
| 73 | - $plugins = get_plugins(); | |
| 162 | + return ( new self() )->get_type_by_transaction_status( self::row_value( $row, 'status' ), $version ); | |
| 163 | + } | |
| 74 | 164 | |
| 75 | - return isset($plugins[$basename]); | |
| 165 | + /** | |
| 166 | + * The status label of a loaded row ("Completed", "Incomplete", "Refunded"), v2 buckets. | |
| 167 | + * | |
| 168 | + * @since 2.4.0 | |
| 169 | + * | |
| 170 | + * @param object|array $row A transaction row. | |
| 171 | + * @return string | |
| 172 | + */ | |
| 173 | + public static function transaction_status_label( $row ): string { | |
| 174 | + return ucfirst( self::transaction_status_type( $row, 'v2' ) ); | |
| 76 | 175 | } |
| 77 | 176 | |
| 78 | 177 | /** |
| 79 | - * check is plugin active or not | |
| 80 | - * | |
| 81 | - * @param $plugin | |
| 82 | - * @return bool | |
| 83 | - * @since 0.0.2 | |
| 84 | - */ | |
| 85 | - public function is_plugin_active($plugin) { | |
| 86 | - if ( !function_exists( 'is_plugin_active' ) ){ | |
| 87 | - require_once( ABSPATH . '/wp-admin/includes/plugin.php' ); | |
| 178 | + * The status colour of a loaded row, v2 buckets: the colour of transaction_status_type()'s bucket. | |
| 179 | + * | |
| 180 | + * @since 2.4.0 | |
| 181 | + * | |
| 182 | + * @param object|array $row A transaction row. | |
| 183 | + * @return string | |
| 184 | + */ | |
| 185 | + public static function transaction_status_color( $row ): string { | |
| 186 | + $helper = new self(); | |
| 187 | + | |
| 188 | + if ( self::is_unconfirmed_legacy_free_checkout( $row ) ) { | |
| 189 | + $buckets = $helper->transaction_statuses_with_type_v2(); | |
| 190 | + | |
| 191 | + return $buckets['incomplete']['color']; | |
| 88 | 192 | } |
| 89 | 193 | |
| 90 | - return is_plugin_active( $plugin ); | |
| 194 | + return $helper->get_color_by_transaction_status( self::row_value( $row, 'status' ), 'v2' ); | |
| 91 | 195 | } |
| 92 | 196 | |
| 93 | - public static function sanitize_bp_field($key, $arr, $default_value='', $type='text'){ | |
| 94 | - $value = $default_value; | |
| 95 | - | |
| 96 | - if( !empty( $arr[ $key ] ) ) { | |
| 97 | - $value = $arr[ $key ]; | |
| 197 | + /** | |
| 198 | + * PHP twin of COUNTABLE_AMOUNT_SQL: what a loaded row contributes to a money total. | |
| 199 | + * | |
| 200 | + * @since 2.4.0 | |
| 201 | + * | |
| 202 | + * @param object|array $row A transaction row. | |
| 203 | + * @return float | |
| 204 | + */ | |
| 205 | + public static function countable_amount( $row ): float { | |
| 206 | + if ( self::LEGACY_FREE_CHECKOUT_STATUS === self::row_value( $row, 'status' ) ) { | |
| 207 | + return 0.0; | |
| 208 | + } | |
| 98 | 209 | |
| 99 | - switch($type){ | |
| 100 | - case 'text': | |
| 101 | - $value = sanitize_text_field( $value ); | |
| 102 | - break; | |
| 103 | - case 'email': | |
| 104 | - $value = sanitize_email( $value ); | |
| 105 | - break; | |
| 106 | - case 'textarea': | |
| 107 | - $value = sanitize_textarea_field( $value ); | |
| 108 | - break; | |
| 109 | - default: | |
| 110 | - $value = sanitize_text_field( $value ); | |
| 111 | - break; | |
| 112 | - } | |
| 113 | - } | |
| 210 | + $amount = self::row_value( $row, 'amount' ); | |
| 114 | 211 | |
| 115 | - return $value; | |
| 212 | + return '' === $amount ? 0.0 : floatval( $amount ); | |
| 116 | 213 | } |
| 117 | 214 | |
| 118 | 215 | /** |
| 119 | - * Get widget settings | |
| 216 | + * Raised total and payment count over loaded transaction rows (the Elementor Fundraising | |
| 217 | + * Campaign widget's progress). | |
| 120 | 218 | * |
| 121 | - * @since 0.0.1 | |
| 219 | + * Counts the same rows CampaignStats counts: an approved status (Stripe `paid`, Paystack | |
| 220 | + * `success`, `Completed`, …) or a confirmed legacy free checkout, which adds 0. It used to | |
| 221 | + * count only `paid`, so Paystack and manually completed donations never moved the bar. | |
| 222 | + * | |
| 223 | + * @since 2.4.0 | |
| 224 | + * | |
| 225 | + * @param mixed $rows Transaction rows (objects or arrays). | |
| 226 | + * @return array{amount: float, count: int} | |
| 122 | 227 | */ |
| 123 | - public function get_better_payment_widget_settings( $page_id, $widget_id ) { | |
| 124 | - $document = \Elementor\Plugin::$instance->documents->get( $page_id ); | |
| 125 | - $settings = []; | |
| 126 | - if ( $document ) { | |
| 127 | - $elements = \Elementor\Plugin::instance()->documents->get( $page_id )->get_elements_data(); | |
| 128 | - $widget_data = $this->find_element_recursive( $elements, $widget_id ); | |
| 129 | - if ( $widget_data ) { | |
| 130 | - $widget = \Elementor\Plugin::instance()->elements_manager->create_element_instance( $widget_data ); | |
| 131 | - if ( $widget ) { | |
| 132 | - $settings = $widget->get_settings_for_display(); | |
| 133 | - } | |
| 228 | + public static function campaign_totals_from_rows( $rows ): array { | |
| 229 | + $totals = array( | |
| 230 | + 'amount' => 0.0, | |
| 231 | + 'count' => 0, | |
| 232 | + ); | |
| 233 | + | |
| 234 | + foreach ( is_array( $rows ) ? $rows : array() as $row ) { | |
| 235 | + $status = self::row_value( $row, 'status' ); | |
| 236 | + | |
| 237 | + $counted = in_array( $status, self::APPROVED_DONATION_STATUSES, true ) | |
| 238 | + || ( self::LEGACY_FREE_CHECKOUT_STATUS === $status && ! self::is_unconfirmed_legacy_free_checkout( $row ) ); | |
| 239 | + | |
| 240 | + if ( ! $counted ) { | |
| 241 | + continue; | |
| 134 | 242 | } |
| 135 | - | |
| 243 | + | |
| 244 | + $totals['amount'] += self::countable_amount( $row ); | |
| 245 | + ++$totals['count']; | |
| 136 | 246 | } |
| 137 | - return $settings; | |
| 247 | + | |
| 248 | + return $totals; | |
| 138 | 249 | } |
| 139 | 250 | |
| 140 | 251 | /** |
| 141 | - * Find element recursively | |
| 252 | + * A scalar column of a row object or array, as a string ('' when absent or not scalar). | |
| 142 | 253 | * |
| 143 | - * @since 0.0.1 | |
| 254 | + * @param object|array $row A transaction row. | |
| 255 | + * @param string $key Column name. | |
| 256 | + * @return string | |
| 144 | 257 | */ |
| 145 | - public function find_element_recursive( $elements, $form_id ) { | |
| 258 | + private static function row_value( $row, string $key ): string { | |
| 259 | + $value = null; | |
| 146 | 260 | |
| 147 | - foreach ( $elements as $element ) { | |
| 148 | - if ( $form_id === $element[ 'id' ] ) { | |
| 149 | - return $element; | |
| 150 | - } | |
| 151 | - | |
| 152 | - if ( !empty( $element[ 'elements' ] ) ) { | |
| 153 | - $element = $this->find_element_recursive( $element[ 'elements' ], $form_id ); | |
| 154 | - | |
| 155 | - if ( $element ) { | |
| 156 | - return $element; | |
| 157 | - } | |
| 158 | - } | |
| 261 | + if ( is_object( $row ) && isset( $row->$key ) ) { | |
| 262 | + $value = $row->$key; | |
| 263 | + } elseif ( is_array( $row ) && isset( $row[ $key ] ) ) { | |
| 264 | + $value = $row[ $key ]; | |
| 159 | 265 | } |
| 160 | 266 | |
| 161 | - return false; | |
| 267 | + return is_scalar( $value ) ? (string) $value : ''; | |
| 162 | 268 | } |
| 163 | 269 | |
| 164 | 270 | /** |
| 165 | - * Get currency list | |
| 271 | + * A row's `form_fields_info` as stored: the serialized string, an already-unserialized array | |
| 272 | + * serialized back, or '' when absent. Only ever searched for LEGACY_CONFIRMATION_MARKER. | |
| 166 | 273 | * |
| 167 | - * @since 0.0.2 | |
| 274 | + * @param object|array $row A transaction row. | |
| 275 | + * @return string | |
| 168 | 276 | */ |
| 169 | - public function get_currency_list(){ | |
| 170 | - $currency_list = [ | |
| 171 | - 'AED' => 'AED', | |
| 172 | - 'AUD' => 'AUD', | |
| 173 | - 'CAD' => 'CAD', | |
| 174 | - 'CZK' => 'CZK', | |
| 175 | - 'DKK' => 'DKK', | |
| 176 | - 'EUR' => 'EUR', | |
| 177 | - 'HKD' => 'HKD', | |
| 178 | - 'HUF' => 'HUF', | |
| 179 | - 'ILS' => 'ILS', | |
| 180 | - 'JPY' => 'JPY', | |
| 181 | - 'MXN' => 'MXN', | |
| 182 | - 'NOK' => 'NOK', | |
| 183 | - 'NZD' => 'NZD', | |
| 184 | - 'PHP' => 'PHP', | |
| 185 | - 'PLN' => 'PLN', | |
| 186 | - 'GBP' => 'GBP', | |
| 187 | - 'RUB' => 'RUB', | |
| 188 | - 'SGD' => 'SGD', | |
| 189 | - 'SEK' => 'SEK', | |
| 190 | - 'CHF' => 'CHF', | |
| 191 | - 'TWD' => 'TWD', | |
| 192 | - 'THB' => 'THB', | |
| 193 | - 'USD' => 'USD', | |
| 194 | - 'ZAR' => 'ZAR' | |
| 195 | - ]; | |
| 277 | + private static function form_fields_info_string( $row ): string { | |
| 278 | + $value = null; | |
| 196 | 279 | |
| 197 | - return $currency_list; | |
| 280 | + if ( is_object( $row ) && isset( $row->form_fields_info ) ) { | |
| 281 | + $value = $row->form_fields_info; | |
| 282 | + } elseif ( is_array( $row ) && isset( $row['form_fields_info'] ) ) { | |
| 283 | + $value = $row['form_fields_info']; | |
| 284 | + } | |
| 285 | + | |
| 286 | + if ( is_array( $value ) ) { | |
| 287 | + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- searched for the marker, never unserialized. | |
| 288 | + return serialize( $value ); | |
| 289 | + } | |
| 290 | + | |
| 291 | + return is_scalar( $value ) ? (string) $value : ''; | |
| 198 | 292 | } |
| 199 | 293 | |
| 294 | + // find_element_recursive() comes from Traits\Helper; this class used to carry | |
| 295 | + // a byte-identical copy of it. | |
| 296 | + | |
| 200 | 297 | public function titleToSnake($text, $divider = '_') { |
| 298 | + // Handle null or empty values to prevent deprecated warnings. | |
| 299 | + if ( null === $text || '' === $text ) { | |
| 300 | + return 'n_a'; | |
| 301 | + } | |
| 302 | + | |
| 303 | + $text = (string) $text; | |
| 201 | 304 | $text = preg_replace('~[^\pL\d]+~u', $divider, $text); |
| 202 | 305 | $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text); |
| 203 | 306 | $text = preg_replace('~[^-\w]+~', '', $text); |
| 204 | 307 | $text = trim($text, $divider); |
| @@ -211,17 +314,8 @@ | ||
| 211 | 314 | |
| 212 | 315 | return $text; |
| 213 | 316 | } |
| 214 | 317 | |
| 215 | - public function arrayToString($array, $separator = ', ', $extraString = ''){ | |
| 216 | - $string = rtrim( implode($separator, $array ), $separator ) ; | |
| 217 | - if($extraString) { | |
| 218 | - $string .= $extraString; | |
| 219 | - } | |
| 220 | - | |
| 221 | - return $string; | |
| 222 | - } | |
| 223 | - | |
| 224 | 318 | public function transaction_statuses_with_type(){ |
| 225 | 319 | $statuses = [ |
| 226 | 320 | 'completed' => [ |
| 227 | 321 | 'statuses' => [ |
| @@ -227,8 +321,14 @@ | ||
| 227 | 321 | 'statuses' => [ |
| 228 | 322 | 'paid', |
| 229 | 323 | 'Completed', |
| 230 | 324 | 'completed', |
| 325 | + // Stripe's own status for a fully-discounted / $0 Checkout | |
| 326 | + // Session (100% coupon or sale, free trial). The payment | |
| 327 | + // IS settled - there was simply nothing to charge - so it | |
| 328 | + // must classify as completed, not fall through to the | |
| 329 | + // unknown-status default. See Helper::get_type_by_transaction_status(). | |
| 330 | + 'no_payment_required', | |
| 231 | 331 | ], |
| 232 | 332 | 'color' => '#0ECA86', |
| 233 | 333 | ], |
| 234 | 334 | 'processing' => [ |
| @@ -247,8 +347,10 @@ | ||
| 247 | 347 | ], |
| 248 | 348 | 'unpaid' => [ |
| 249 | 349 | 'statuses' => [ |
| 250 | 350 | 'unpaid', |
| 351 | + // A payment that failed. | |
| 352 | + 'failed', | |
| 251 | 353 | ], |
| 252 | 354 | 'color' => '#999', |
| 253 | 355 | ], |
| 254 | 356 | 'all' => [ |
| @@ -255,12 +357,14 @@ | ||
| 255 | 357 | 'statuses' => [ |
| 256 | 358 | 'paid', |
| 257 | 359 | 'Completed', |
| 258 | 360 | 'completed', |
| 361 | + 'no_payment_required', | |
| 259 | 362 | 'unpaid', |
| 260 | 363 | 'refunded', |
| 261 | 364 | 'pending', |
| 262 | 365 | 'Pending', |
| 366 | + 'failed', | |
| 263 | 367 | NULL, |
| 264 | 368 | ], |
| 265 | 369 | 'color' => '#999', |
| 266 | 370 | ], |
| @@ -276,8 +380,16 @@ | ||
| 276 | 380 | 'paid', |
| 277 | 381 | 'Completed', |
| 278 | 382 | 'completed', |
| 279 | 383 | 'success', |
| 384 | + // See the v1 note: a $0 Stripe session settles as | |
| 385 | + // 'no_payment_required'. Every status bucket is also a | |
| 386 | + // `status IN (...)` filter in DB::get_transactions(), so a | |
| 387 | + // status missing from BOTH 'completed' and 'incomplete' | |
| 388 | + // is invisible under either tab while still showing on | |
| 389 | + // the unfiltered list - which is exactly how a paid $0 | |
| 390 | + // order looked like a stray "Incomplete" row. | |
| 391 | + 'no_payment_required', | |
| 280 | 392 | ], |
| 281 | 393 | 'color' => '#0ECA86', |
| 282 | 394 | ], |
| 283 | 395 | 'incomplete' => [ |
| @@ -286,8 +398,10 @@ | ||
| 286 | 398 | 'Pending', |
| 287 | 399 | 'unpaid', |
| 288 | 400 | 'incomplete', |
| 289 | 401 | 'Incomplete', |
| 402 | + // A payment that failed. | |
| 403 | + 'failed', | |
| 290 | 404 | NULL, |
| 291 | 405 | ], |
| 292 | 406 | 'color' => '#FFDA15', |
| 293 | 407 | ], |
| @@ -301,8 +415,9 @@ | ||
| 301 | 415 | 'statuses' => [ |
| 302 | 416 | 'paid', |
| 303 | 417 | 'Completed', |
| 304 | 418 | 'completed', |
| 419 | + 'no_payment_required', | |
| 305 | 420 | 'unpaid', |
| 306 | 421 | 'refunded', |
| 307 | 422 | 'pending', |
| 308 | 423 | 'Pending', |
| @@ -308,8 +423,9 @@ | ||
| 308 | 423 | 'Pending', |
| 309 | 424 | 'success', |
| 310 | 425 | 'incomplete', |
| 311 | 426 | 'Incomplete', |
| 427 | + 'failed', | |
| 312 | 428 | NULL, |
| 313 | 429 | ], |
| 314 | 430 | 'color' => '#999', |
| 315 | 431 | ], |
| @@ -366,28 +482,8 @@ | ||
| 366 | 482 | return $color; |
| 367 | 483 | |
| 368 | 484 | } |
| 369 | 485 | |
| 370 | - public function get_color_by_transaction_type($status_type = '', $version = 'v1'){ | |
| 371 | - if('v1' === $version){ | |
| 372 | - $statuses = $this->transaction_statuses_with_type(); | |
| 373 | - } else { | |
| 374 | - $statuses = $this->transaction_statuses_with_type_v2(); | |
| 375 | - } | |
| 376 | - | |
| 377 | - $color = ''; | |
| 378 | - if(!empty($status_type)){ | |
| 379 | - foreach($statuses as $type => $statuses_and_color){ | |
| 380 | - if($type == $status_type){ | |
| 381 | - $color = $statuses_and_color['color']; | |
| 382 | - break; | |
| 383 | - } | |
| 384 | - } | |
| 385 | - } | |
| 386 | - | |
| 387 | - return $color; | |
| 388 | - } | |
| 389 | - | |
| 390 | 486 | public function get_statuses_by_transaction_type($status_type = 'all', $version = 'v1'){ |
| 391 | 487 | if('v1' === $version){ |
| 392 | 488 | $statuses = $this->transaction_statuses_with_type(); |
| 393 | 489 | } else { |
| @@ -417,30 +513,129 @@ | ||
| 417 | 513 | $statuses_types = array_keys($statuses); |
| 418 | 514 | |
| 419 | 515 | return $statuses_types; |
| 420 | 516 | } |
| 517 | + | |
| 518 | + /** | |
| 519 | + * Get interval text for recurring and split payment | |
| 520 | + * | |
| 521 | + * Lite never calls this. Better Payment Pro <= 1.4.3's layout partials did, from | |
| 522 | + * templates that no longer load (Pro 1.4.3 fills no `layout_template` filter), and | |
| 523 | + * tests/Unit/ProCouplingContractTest.php still pins it. Pro 1.4.4+ uses its own | |
| 524 | + * Admin\Elementor\LayoutRenderer::interval_text(). | |
| 525 | + * | |
| 526 | + * @since 1.3.0 | |
| 527 | + * @deprecated 2.4.0 Kept as belt-and-braces for Pro 1.4.3. Delete once no supported | |
| 528 | + * site can run it. | |
| 529 | + */ | |
| 530 | + public function get_interval_text( $interval = '' ){ | |
| 531 | + $interval_text = ''; | |
| 532 | + | |
| 533 | + switch( $interval ) { | |
| 534 | + case 'day': | |
| 535 | + $interval_text = 'Daily'; | |
| 536 | + break; | |
| 537 | + | |
| 538 | + case 'month': | |
| 539 | + case 'year': | |
| 540 | + $interval_text = $interval . 'ly'; | |
| 541 | + break; | |
| 542 | + | |
| 543 | + default : | |
| 544 | + $interval_text = ! empty( $interval ) ? "Per " . esc_html( $interval ) : ""; | |
| 545 | + break; | |
| 546 | + } | |
| 547 | + | |
| 548 | + return esc_html( ucfirst( $interval_text ) ); | |
| 549 | + } | |
| 550 | + | |
| 551 | + /** | |
| 552 | + * The payment type label of a transaction, as the User Dashboard REST rows and | |
| 553 | + * the CSV export print it. | |
| 554 | + * | |
| 555 | + * Lite's own checkouts only take one-time payments, so Lite says 'One Time'. | |
| 556 | + * `better_payment/transaction/payment_type` ($label, $form_fields_info) relabels | |
| 557 | + * a row: Pro returns 'Subscription' for its recurring and split payments. The CSV | |
| 558 | + * import reads the label back, so keep these two strings. | |
| 559 | + * | |
| 560 | + * @since 2.4.0 | |
| 561 | + * | |
| 562 | + * @param mixed $form_fields_info The transaction's unserialized form_fields_info. | |
| 563 | + * @return string | |
| 564 | + */ | |
| 565 | + public static function payment_type_label( $form_fields_info ): string { | |
| 566 | + $form_fields_info = is_array( $form_fields_info ) ? $form_fields_info : []; | |
| 567 | + $label = 'One Time'; | |
| 568 | + | |
| 569 | + $filtered = apply_filters( 'better_payment/transaction/payment_type', $label, $form_fields_info ); | |
| 570 | + | |
| 571 | + return is_string( $filtered ) ? $filtered : $label; | |
| 572 | + } | |
| 573 | + | |
| 574 | + /** | |
| 575 | + * Output the page header, compatible with both classic and block themes. | |
| 576 | + * | |
| 577 | + * Block themes don't use get_header() — they rely on block template parts. | |
| 578 | + * We detect this with wp_is_block_theme() and render the header template | |
| 579 | + * part via do_blocks() so it appears correctly on CPT single pages that | |
| 580 | + * bypass the theme's block templates. | |
| 581 | + * | |
| 582 | + * @param string $block_header Pre-rendered header markup from do_blocks() (block themes only). | |
| 583 | + * @return void | |
| 584 | + */ | |
| 585 | + public static function render_header( string $block_header = '' ): void { | |
| 586 | + if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) { | |
| 587 | + ?> | |
| 588 | + <!doctype html> | |
| 589 | + <html <?php language_attributes(); ?>> | |
| 590 | + <head> | |
| 591 | + <meta charset="<?php bloginfo( 'charset' ); ?>"> | |
| 592 | + <?php wp_head(); ?> | |
| 593 | + </head> | |
| 594 | + <body <?php body_class(); ?>> | |
| 595 | + <?php | |
| 596 | + wp_body_open(); | |
| 597 | + echo $block_header; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | |
| 598 | + } else { | |
| 599 | + get_header(); | |
| 600 | + } | |
| 601 | + } | |
| 602 | + | |
| 603 | + /** | |
| 604 | + * Output the page footer, compatible with both classic and block themes. | |
| 605 | + * | |
| 606 | + * @param string $block_footer Pre-rendered footer markup from do_blocks() (block themes only). | |
| 607 | + * @return void | |
| 608 | + */ | |
| 609 | + public static function render_footer( string $block_footer = '' ): void { | |
| 610 | + if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) { | |
| 611 | + echo $block_footer; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | |
| 612 | + wp_footer(); | |
| 613 | + ?> | |
| 614 | + </body> | |
| 615 | + </html> | |
| 616 | + <?php | |
| 617 | + } else { | |
| 618 | + get_footer(); | |
| 619 | + } | |
| 620 | + } | |
| 421 | 621 | } |
| 422 | 622 | |
| 423 | 623 | |
| 424 | -//Helper Functions | |
| 425 | -if( !function_exists('better_payment_dd') ){ | |
| 426 | - function better_payment_dd($data, $show_query = 0, $print_only = 0) { | |
| 624 | +// Helper Functions | |
| 625 | +if ( ! function_exists( 'better_payment_dd' ) ) { | |
| 626 | + function better_payment_dd( $data, $show_query = 0, $print_only = 0 ) { | |
| 427 | 627 | global $wpdb; |
| 428 | 628 | |
| 429 | - if(1 === $show_query){ | |
| 430 | - // Print last SQL query string | |
| 629 | + if ( 1 === $show_query ) { | |
| 431 | 630 | echo wp_kses_post( $wpdb->last_query ); |
| 432 | - | |
| 433 | - //Print last SQL query result | |
| 434 | 631 | echo wp_kses_post( $wpdb->last_result ); |
| 435 | - | |
| 436 | - //Print last SQL query Error | |
| 437 | 632 | echo wp_kses_post( $wpdb->last_error ); |
| 438 | 633 | } |
| 439 | - | |
| 440 | - echo "<pre>"; | |
| 441 | - print_r($data); | |
| 442 | - if(!$print_only){ | |
| 443 | - wp_die('Printed!'); | |
| 634 | + | |
| 635 | + echo '<pre>'; | |
| 636 | + print_r( $data ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r | |
| 637 | + if ( ! $print_only ) { | |
| 638 | + wp_die( 'Printed!' ); | |
| 444 | 639 | } |
| 445 | 640 | } |
| 446 | 641 | } |