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/Admin/DB.php +417 -92 1.0.4 → 2.4.1 View file →
@@ -19,8 +19,16 @@
19 19 * @since 0.0.1
20 20 */
21 21 class DB {
22 22
23 + /**
24 + * Helper's counting rule (an unconfirmed pre-2.4.0 `no_payment_required` row is not counted; a
25 + * confirmed one adds 0), aliased so the two deprecated queries below spell it `self::…` exactly
26 + * as Pro's AnalyticsModel copies do (AnalyticsModelParityTest compares the bodies).
27 + */
28 + private const COUNTED_ROW_SQL = Helper::COUNTED_ROW_SQL;
29 + private const COUNTABLE_AMOUNT_SQL = Helper::COUNTABLE_AMOUNT_SQL;
30 +
23 31 public static function get_table_name() {
24 32 global $wpdb;
25 33
26 34 $table = "{$wpdb->prefix}better_payment";
@@ -34,57 +42,84 @@
34 42 * @since 0.0.1
35 43 */
36 44 public static function default_settings() {
37 45 return apply_filters('better_payment_option_default_settings', array(
38 - 'better_payment_settings_general_general_paypal' => esc_html__('yes', 'better-payment'),
39 - 'better_payment_settings_general_general_stripe' => esc_html__('yes', 'better-payment'),
40 - 'better_payment_settings_general_general_paystack' => esc_html__('', 'better-payment'),
41 - 'better_payment_settings_general_general_email' => esc_html__('yes', 'better-payment'),
42 - 'better_payment_settings_general_general_currency' => esc_html__('USD', 'better-payment'),
46 + 'better_payment_settings_general_general_paypal' => 'yes',
47 + 'better_payment_settings_general_general_stripe' => 'yes',
48 + 'better_payment_settings_general_general_paystack' => '',
49 + 'better_payment_settings_general_general_user_dashboard' => 'yes',
50 + 'better_payment_settings_general_general_fundraising_campaign' => 'yes',
51 + 'better_payment_settings_general_general_email' => 'yes',
52 + 'better_payment_settings_general_general_currency' => 'USD',
43 53
44 - 'better_payment_settings_general_email_to' => esc_html__('', 'better-payment'),
45 - 'better_payment_settings_general_email_subject' => esc_html__('', 'better-payment'),
46 - 'better_payment_settings_general_email_message_admin' => esc_html__('', 'better-payment'),
47 - 'better_payment_settings_general_email_from_email' => esc_html__('', 'better-payment'),
48 - 'better_payment_settings_general_email_from_name' => esc_html__('', 'better-payment'),
49 - 'better_payment_settings_general_email_reply_to' => esc_html__('', 'better-payment'),
50 - 'better_payment_settings_general_email_cc' => esc_html__('', 'better-payment'),
51 - 'better_payment_settings_general_email_bcc' => esc_html__('', 'better-payment'),
52 - 'better_payment_settings_general_email_send_as' => esc_html__('html', 'better-payment'),
54 + 'better_payment_settings_general_email_to' => '',
55 + 'better_payment_settings_general_email_subject' => '',
56 + 'better_payment_settings_general_email_message_admin' => '',
57 + 'better_payment_settings_general_email_from_email' => '',
58 + 'better_payment_settings_general_email_from_name' => '',
59 + 'better_payment_settings_general_email_reply_to' => '',
60 + 'better_payment_settings_general_email_cc' => '',
61 + 'better_payment_settings_general_email_bcc' => '',
62 + 'better_payment_settings_general_email_send_as' => 'html',
53 63
54 - 'better_payment_settings_general_email_subject_customer' => esc_html__('', 'better-payment'),
55 - 'better_payment_settings_general_email_message_customer' => esc_html__('', 'better-payment'),
56 - 'better_payment_settings_general_email_from_email_customer' => esc_html__('', 'better-payment'),
57 - 'better_payment_settings_general_email_from_name_customer' => esc_html__('', 'better-payment'),
58 - 'better_payment_settings_general_email_reply_to_customer' => esc_html__('', 'better-payment'),
59 - 'better_payment_settings_general_email_cc_customer' => esc_html__('', 'better-payment'),
60 - 'better_payment_settings_general_email_bcc_customer' => esc_html__('', 'better-payment'),
61 - 'better_payment_settings_general_email_send_as_customer' => esc_html__('html', 'better-payment'),
64 + 'better_payment_settings_general_email_subject_customer' => '',
65 + 'better_payment_settings_general_email_message_customer' => '',
66 + 'better_payment_settings_general_email_from_email_customer' => '',
67 + 'better_payment_settings_general_email_from_name_customer' => '',
68 + 'better_payment_settings_general_email_reply_to_customer' => '',
69 + 'better_payment_settings_general_email_cc_customer' => '',
70 + 'better_payment_settings_general_email_bcc_customer' => '',
71 + 'better_payment_settings_general_email_send_as_customer' => 'html',
62 72
63 - 'better_payment_settings_payment_paypal_live_mode' => esc_html__('no', 'better-payment'),
64 - 'better_payment_settings_payment_paypal_email' => esc_html__('', 'better-payment'),
65 - 'better_payment_settings_opt_in' => esc_html__('', 'better-payment'),
66 - 'better_payment_settings_payment_paypal_live_client_id' => esc_html__('', 'better-payment'),
67 - 'better_payment_settings_payment_paypal_test_client_id' => esc_html__('', 'better-payment'),
68 - 'better_payment_settings_payment_paypal_live_secret' => esc_html__('', 'better-payment'),
69 - 'better_payment_settings_payment_paypal_test_secret' => esc_html__('', 'better-payment'),
70 - 'better_payment_settings_payment_stripe_live_mode' => esc_html__('no', 'better-payment'),
71 - 'better_payment_settings_payment_stripe_live_public' => esc_html__('', 'better-payment'),
72 - 'better_payment_settings_payment_stripe_live_secret' => esc_html__('', 'better-payment'),
73 - 'better_payment_settings_payment_stripe_test_public' => esc_html__('', 'better-payment'),
74 - 'better_payment_settings_payment_stripe_test_secret' => esc_html__('', 'better-payment'),
75 - 'better_payment_settings_payment_paystack_live_mode' => esc_html__('no', 'better-payment'),
76 - 'better_payment_settings_payment_paystack_live_public' => esc_html__('', 'better-payment'),
77 - 'better_payment_settings_payment_paystack_live_secret' => esc_html__('', 'better-payment'),
78 - 'better_payment_settings_payment_paystack_test_public' => esc_html__('', 'better-payment'),
79 - 'better_payment_settings_payment_paystack_test_secret' => esc_html__('', 'better-payment'),
73 + 'better_payment_settings_payment_paypal_live_mode' => 'no',
74 + 'better_payment_settings_payment_paypal_email' => '',
75 + 'better_payment_settings_opt_in' => '',
76 + 'better_payment_settings_payment_paypal_live_client_id' => '',
77 + 'better_payment_settings_payment_paypal_test_client_id' => '',
78 + 'better_payment_settings_payment_paypal_live_secret' => '',
79 + 'better_payment_settings_payment_paypal_test_secret' => '',
80 + 'better_payment_settings_payment_stripe_live_mode' => 'no',
81 + 'better_payment_settings_payment_stripe_live_public' => '',
82 + 'better_payment_settings_payment_stripe_live_secret' => '',
83 + 'better_payment_settings_payment_stripe_test_public' => '',
84 + 'better_payment_settings_payment_stripe_test_secret' => '',
85 + 'better_payment_settings_payment_paystack_live_mode' => 'no',
86 + 'better_payment_settings_payment_paystack_live_public' => '',
87 + 'better_payment_settings_payment_paystack_live_secret' => '',
88 + 'better_payment_settings_payment_paystack_test_public' => '',
89 + 'better_payment_settings_payment_paystack_test_secret' => '',
90 +
91 + // AI Assistant (Campaign Builder). See includes/AI/.
92 + 'better_payment_settings_ai_enabled' => '',
93 + 'better_payment_settings_ai_provider' => 'openai',
94 + 'better_payment_settings_ai_model' => '',
95 + 'better_payment_settings_ai_image_model' => '',
96 + 'better_payment_settings_ai_temperature' => '0.7',
97 + 'better_payment_settings_ai_max_tokens' => '4096',
98 + 'better_payment_settings_ai_system_prompt' => '',
99 + 'better_payment_settings_ai_api_key_openai' => '',
100 + 'better_payment_settings_ai_api_key_claude' => '',
101 + 'better_payment_settings_ai_api_key_gemini' => '',
102 + 'better_payment_settings_ai_api_key_openrouter' => '',
103 +
104 + // E-commerce > Subscription. Booleans follow the 'yes'/'no' string
105 + // convention used across this settings array.
106 + 'better_payment_settings_ecommerce_subscription_renewal_process' => 'auto',
107 + 'better_payment_settings_ecommerce_subscription_stripe_auto_renew' => 'yes',
108 + 'better_payment_settings_ecommerce_subscription_auto_renewal_toggle' => 'yes',
109 + 'better_payment_settings_ecommerce_subscription_myaccount_tab' => 'yes',
110 + 'better_payment_settings_ecommerce_subscription_active_role' => 'subscriber',
111 + 'better_payment_settings_ecommerce_subscription_inactive_role' => 'customer',
80 112 ));
81 113 }
82 114 /**
83 115 * Get all settings value from options table.
84 116 *
117 + * Returns the full settings array when $name is empty, otherwise the single
118 + * value for that key (which may be a string, array, etc.).
119 + *
85 120 * @param string $name
86 - * @return array
121 + * @return mixed
87 122 * @since 0.0.1
88 123 */
89 124 public static function get_settings($name = '')
90 125 {
@@ -89,8 +124,11 @@
89 124 public static function get_settings($name = '')
90 125 {
91 126 $settings = get_option('better_payment_settings', true);
92 127 $default = self::default_settings();
128 +
129 + $settings = wp_parse_args($settings, $default);
130 +
93 131 if (!empty($name) && isset($settings[$name])) {
94 132 return $settings[$name];
95 133 }
96 134
@@ -118,11 +156,43 @@
118 156 return update_option('better_payment_settings', $value);
119 157 }
120 158
121 159 /**
160 + * The UTC `payment_date` bounds for a date range picked in site time.
161 + *
162 + * The admin date filter sends whole days (`Y-m-d`), picked on the site's
163 + * calendar. Compared as-is, `BETWEEN '2026-10-11' AND '2026-10-11'` matched
164 + * only rows at exactly midnight — so the last day of every range was dropped
165 + * — and the days were UTC days, not the site's. A day bound is widened to the
166 + * whole site day (00:00:00 – 23:59:59) and converted to UTC, which is how
167 + * `payment_date` is stored. A bound that already carries a time is passed
168 + * through unchanged.
169 + *
170 + * @since 2.4.1
171 + *
172 + * @param string $from Lower bound.
173 + * @param string $to Upper bound.
174 + * @return string[] [ from, to ].
175 + */
176 + public static function payment_date_range( $from, $to ) {
177 + $from = trim( (string) $from );
178 + $to = trim( (string) $to );
179 +
180 + if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $from ) ) {
181 + $from = get_gmt_from_date( $from . ' 00:00:00', 'Y-m-d H:i:s' );
182 + }
183 +
184 + if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $to ) ) {
185 + $to = get_gmt_from_date( $to . ' 23:59:59', 'Y-m-d H:i:s' );
186 + }
187 +
188 + return array( $from, $to );
189 + }
190 +
191 + /**
122 192 * Get all transactions from better payment table.
123 193 *
124 - * @param string $args
194 + * @param array|string $args Filters, as wp_parse_args() reads them (every caller passes an array).
125 195 * @return array
126 196 * @since 0.0.1
127 197 */
128 198 public static function get_transactions($args = [], $count_only=0, $version = 'v1', $fetchNull = 0, $transaction_type = '')
@@ -133,10 +203,11 @@
133 203 $defaults = array(
134 204 'search_text' => '',
135 205 'payment_date_from' => '',
136 206 'payment_date_to' => '',
137 - 'order_by' => 'id',
207 + 'order_by' => 'payment_date',
138 208 'order' => 'DESC',
209 + 'currency' => 'all',
139 210 'source' => '',
140 211 'paged' => 1,
141 212 'per_page' => 20,
142 213 'offset' => 0,
@@ -182,10 +253,21 @@
182 253 $statuses_by_transaction_type = array();
183 254
184 255 if( !empty($transaction_type) ){
185 256 $statuses_by_types = $better_payment_helper_obj->transaction_statuses_with_type_v2();
186 - $statuses_by_transaction_type = isset($statuses_by_types[$transaction_type]['statuses']) ? $statuses_by_types[$transaction_type]['statuses'] : $defaults['status'];
187 - $status = '';
257 + $transaction_type = is_string( $transaction_type ) ? $transaction_type : 'all';
258 +
259 + if ( 'null' === $transaction_type ) {
260 + // The count twin of `status=null` below.
261 + $status = 'null';
262 + $transaction_type = '';
263 + } else {
264 + // An unknown type is every row, as an unknown `status` is. It used to fall back to the
265 + // string 'all' and fatal in array_map() below.
266 + $transaction_type = isset( $statuses_by_types[ $transaction_type ] ) ? $transaction_type : 'all';
267 + $statuses_by_transaction_type = $statuses_by_types[ $transaction_type ]['statuses'];
268 + $status = '';
269 + }
188 270 }
189 271
190 272 $whereQuery = '';
191 273
@@ -190,9 +272,9 @@
190 272 $whereQuery = '';
191 273
192 274 $valid_search_text = $search_text != '' && strlen($search_text) >= 2;
193 275 if($valid_search_text){
194 - $whereQuery = $wpdb->prepare(' AND email LIKE %s OR form_fields_info LIKE %s OR transaction_id LIKE %s OR amount LIKE %s OR source LIKE %s ', $search_text, $search_text ,$search_text, $search_text, $search_text );
276 + $whereQuery = $wpdb->prepare(' AND ( email LIKE %s OR form_fields_info LIKE %s OR transaction_id LIKE %s OR amount LIKE %s OR source LIKE %s ) ', $search_text, $search_text ,$search_text, $search_text, $search_text );
195 277 }
196 278
197 279 if( $args['source'] != '' ){
198 280 $whereQuery .= $wpdb->prepare(" AND source = %s", $args['source']);
@@ -197,8 +279,12 @@
197 279 if( $args['source'] != '' ){
198 280 $whereQuery .= $wpdb->prepare(" AND source = %s", $args['source']);
199 281 }
200 282
283 + if( $args['currency'] != 'all' ){
284 + $whereQuery .= $wpdb->prepare(" AND currency = %s", $args['currency']);
285 + }
286 +
201 287 if( $status != 'all' ){
202 288 if($status == 'null'){
203 289 $whereQuery .= esc_sql(" AND status is NULL");
204 290 }else {
@@ -215,19 +301,37 @@
215 301
216 302 $statuses = array_map(function($status) {
217 303 return "'" . esc_sql($status) . "'";
218 304 }, $statuses_by_type);
219 -
305 +
220 306 $statuses = implode(',', $statuses);
221 -
222 - $nullDataQuery = $fetchNull ? " OR status IS NULL " : '';
223 - $whereQuery .= " AND ( status IN ( $statuses ) " . esc_sql($nullDataQuery) . " ) ";
307 +
308 + $bucket = $transaction_type ? $transaction_type : $status;
309 +
310 + // NULL is the incomplete bucket's own status (PayPal's pending sentinel): `IN ()`
311 + // cannot match it, so the Incomplete tab always ORs it in and lists the rows its
312 + // total and stat card count.
313 + $nullDataQuery = ( $fetchNull || 'incomplete' === $bucket ) ? " OR status IS NULL " : '';
314 +
315 + // The counting rule, for the tabs: an unconfirmed pre-2.4.0 `no_payment_required`
316 + // row is in no count, so it is listed under Incomplete, never Completed, as
317 + // Helper::transaction_status_type() labels it. A confirmed one stays Completed;
318 + // `refunded` is unchanged. Helper constants only (no request data, no `%`).
319 + $unconfirmed_sql = 'incomplete' === $bucket ? ' OR ' . Helper::UNCONFIRMED_LEGACY_SQL . ' ' : '';
320 + $counted_sql = 'completed' === $bucket ? ' AND ' . Helper::COUNTED_ROW_SQL . ' ' : '';
321 +
322 + // `all` is every row, as the All tab's list is: its `IN ()` list would leave NULL and
323 + // unlisted statuses out of the total.
324 + if ( 'all' !== $bucket ) {
325 + $whereQuery .= " AND ( status IN ( $statuses ) " . esc_sql($nullDataQuery) . $unconfirmed_sql . " ) " . $counted_sql;
326 + }
224 327 }
225 328 }
226 329 }
227 330
228 331 if( $args['payment_date_from'] != '' && $args['payment_date_to'] != ''){
229 - $whereQuery .= $wpdb->prepare(" AND payment_date BETWEEN %s AND %s", $args['payment_date_from'], $args['payment_date_to']);
332 + $range = self::payment_date_range( $args['payment_date_from'], $args['payment_date_to'] );
333 + $whereQuery .= $wpdb->prepare(" AND payment_date BETWEEN %s AND %s", $range[0], $range[1]);
230 334 }
231 335
232 336 if($count_only === 1){
233 337 if($valid_search_text){
@@ -300,12 +404,12 @@
300 404
301 405 /**
302 406 * Get transactions analytics
303 407 *
304 - * @return int
408 + * @return array<string, mixed>
305 409 * @since 0.0.1
306 410 */
307 - public static function get_transactions_analytics($filtered_transactions = array())
411 + public static function get_transactions_analytics_dashboard($filtered_transactions = array())
308 412 {
309 413 global $wpdb;
310 414 $transaction_analytics = [];
311 415
@@ -311,20 +415,47 @@
311 415
312 416 $table = self::get_table_name();
313 417 $better_payment_db_obj = new DB();
314 418
315 - $total_transactions = $better_payment_db_obj->get_transactions_amount_by_statuses(array(), '', '', 'count', 1, $filtered_transactions );
419 + $total_transactions_count = 0;
420 + $completed_transactions_count = 0;
421 + $incomplete_transactions_count = 0;
422 + $refunded_transactions_count = 0;
316 423
317 - $completed_statuses = $better_payment_db_obj->allowed_statuses('completed', 'v2');
318 - $completed_transactions = $better_payment_db_obj->get_transactions_amount_by_statuses( $completed_statuses, '', '', 'count', 0, $filtered_transactions );
424 + $total_transactions_amount = 0;
425 + $completed_transactions_amount = 0;
426 + $incomplete_transactions_amount = 0;
427 + $refunded_transactions_amount = 0;
319 428
320 - $incomplete_statuses = $better_payment_db_obj->allowed_statuses('incomplete', 'v2');
321 - $incomplete_transactions = $better_payment_db_obj->get_transactions_amount_by_statuses( $incomplete_statuses, '', '', 'count', 1, $filtered_transactions );
429 + if ( is_array( $filtered_transactions ) && count( $filtered_transactions ) ) {
430 + $total_transactions_count = $better_payment_db_obj->get_transactions_amount_by_statuses(array(), '', '', 'count', 1, $filtered_transactions );
431 + $total_transactions_amount = $better_payment_db_obj->get_transactions_amount_by_statuses(array(), '', '', 'amount', 1, $filtered_transactions );
432 +
433 + $completed_statuses = $better_payment_db_obj->allowed_statuses('completed', 'v2');
434 + $completed_transactions_count = $better_payment_db_obj->get_transactions_amount_by_statuses( $completed_statuses, '', '', 'count', 0, $filtered_transactions );
435 + $completed_transactions_amount = $better_payment_db_obj->get_transactions_amount_by_statuses( $completed_statuses, '', '', 'amount', 0, $filtered_transactions );
436 +
437 + $incomplete_statuses = $better_payment_db_obj->allowed_statuses('incomplete', 'v2');
438 + $incomplete_transactions_count = $better_payment_db_obj->get_transactions_amount_by_statuses( $incomplete_statuses, '', '', 'count', 1, $filtered_transactions );
439 + $incomplete_transactions_amount = $better_payment_db_obj->get_transactions_amount_by_statuses( $incomplete_statuses, '', '', 'amount', 1, $filtered_transactions );
440 +
441 + $refunded_statuses = $better_payment_db_obj->allowed_statuses('refunded', 'v2');
442 + $refunded_transactions_count = $better_payment_db_obj->get_transactions_amount_by_statuses( $refunded_statuses, '', '', 'count', 0, $filtered_transactions );
443 + $refunded_transactions_amount = $better_payment_db_obj->get_transactions_amount_by_statuses( $refunded_statuses, '', '', 'amount', 0, $filtered_transactions );
444 + }
322 445
323 - $transaction_analytics['total_transactions'] = $total_transactions;
324 - $transaction_analytics['completed_transactions'] = $completed_transactions;
325 - $transaction_analytics['incomplete_transactions'] = $incomplete_transactions;
446 + $transaction_analytics['total_transactions_count'] = $total_transactions_count;
447 + $transaction_analytics['completed_transactions_count'] = $completed_transactions_count;
448 + $transaction_analytics['incomplete_transactions_count'] = $incomplete_transactions_count;
449 + $transaction_analytics['refunded_transactions_count'] = $refunded_transactions_count;
326 450
451 + $transaction_analytics['total_transactions_amount'] = $total_transactions_amount;
452 + $transaction_analytics['completed_transactions_amount'] = $completed_transactions_amount;
453 + $transaction_analytics['incomplete_transactions_amount'] = $incomplete_transactions_amount;
454 + $transaction_analytics['refunded_transactions_amount'] = $refunded_transactions_amount;
455 +
456 + // Recent transactions
457 +
327 458 return $transaction_analytics;
328 459 }
329 460
330 461 /**
@@ -349,10 +480,168 @@
349 480 );
350 481
351 482 return $item;
352 483 }
484 +
485 + /**
486 + * Get analytics counts and amounts for a specific user via a single SQL aggregate query.
487 + * Returns the same shape as get_transactions_analytics_dashboard() without loading all rows.
488 + *
489 + * An unconfirmed pre-2.4.0 `no_payment_required` row is left out of every figure; a confirmed
490 + * one counts as completed and adds 0 to the amounts (Helper::COUNTED_ROW_SQL /
491 + * Helper::COUNTABLE_AMOUNT_SQL).
492 + *
493 + * @param string $email
494 + * @return array
495 + */
496 + public static function get_user_analytics_by_email( $email ) {
497 + global $wpdb;
498 + $table = self::get_table_name();
499 + $email = sanitize_text_field( $email );
500 + $ffi_like = '%' . $wpdb->esc_like( '"' . $email . '"' ) . '%';
353 501
502 + // Helper constants only: no request data, and no `%` for prepare() to misread.
503 + $countable_amount = 'CAST(' . Helper::COUNTABLE_AMOUNT_SQL . ' AS DECIMAL(10,2))';
504 + $completed = "status IN ('paid','Completed','completed','success','" . Helper::LEGACY_FREE_CHECKOUT_STATUS . "')";
505 + $counted_row = Helper::COUNTED_ROW_SQL;
506 +
507 + $row = $wpdb->get_row(
508 + $wpdb->prepare(
509 + "SELECT
510 + COUNT(*) AS total_count,
511 + SUM({$countable_amount}) AS total_amount,
512 + SUM(CASE WHEN {$completed}
513 + THEN 1 ELSE 0 END) AS completed_count,
514 + SUM(CASE WHEN {$completed}
515 + THEN {$countable_amount} ELSE 0 END) AS completed_amount,
516 + SUM(CASE WHEN status IN ('pending','Pending','unpaid','incomplete','Incomplete','failed') OR status IS NULL
517 + THEN 1 ELSE 0 END) AS incomplete_count,
518 + SUM(CASE WHEN status IN ('pending','Pending','unpaid','incomplete','Incomplete','failed') OR status IS NULL
519 + THEN CAST(amount AS DECIMAL(10,2)) ELSE 0 END) AS incomplete_amount,
520 + SUM(CASE WHEN status = 'refunded' THEN 1 ELSE 0 END) AS refunded_count,
521 + SUM(CASE WHEN status = 'refunded'
522 + THEN CAST(amount AS DECIMAL(10,2)) ELSE 0 END) AS refunded_amount
523 + FROM $table WHERE ( email = %s OR form_fields_info LIKE %s ) AND {$counted_row}",
524 + $email,
525 + $ffi_like
526 + )
527 + );
528 +
529 + if ( ! $row ) {
530 + $row = (object) array_fill_keys(
531 + array( 'total_count', 'total_amount', 'completed_count', 'completed_amount', 'incomplete_count', 'incomplete_amount', 'refunded_count', 'refunded_amount' ),
532 + 0
533 + );
534 + }
535 +
536 + return [
537 + 'total_transactions_count' => (int) $row->total_count,
538 + 'total_transactions_amount' => (float) $row->total_amount,
539 + 'completed_transactions_count' => (int) $row->completed_count,
540 + 'completed_transactions_amount' => (float) $row->completed_amount,
541 + 'incomplete_transactions_count' => (int) $row->incomplete_count,
542 + 'incomplete_transactions_amount' => (float) $row->incomplete_amount,
543 + 'refunded_transactions_count' => (int) $row->refunded_count,
544 + 'refunded_transactions_amount' => (float) $row->refunded_amount,
545 + ];
546 + }
547 +
354 548 /**
549 + * Get paginated transactions for a specific user email.
550 + *
551 + * @param string $email
552 + * @param array $args page, per_page, type ('transactions'|'subscriptions')
553 + * @return array { transactions: array, total: int, pages: int }
554 + */
555 + public static function get_user_transactions_paginated( $email, $args = [] ) {
556 + global $wpdb;
557 +
558 + $defaults = [
559 + 'page' => 1,
560 + 'per_page' => 10,
561 + 'type' => 'transactions',
562 + ];
563 + $args = wp_parse_args( $args, $defaults );
564 + $table = self::get_table_name();
565 + $email = sanitize_text_field( $email );
566 + $page = max( 1, intval( $args['page'] ) );
567 + $per_page = max( 1, intval( $args['per_page'] ) );
568 + $offset = ( $page - 1 ) * $per_page;
569 +
570 + $type_where = self::user_transactions_type_where( (string) $args['type'] );
571 + if ( null === $type_where ) {
572 + // Nothing defines this row type (e.g. 'subscriptions' without Pro): answer
573 + // with no rows, never with every row the user owns.
574 + return [
575 + 'transactions' => [],
576 + 'total' => 0,
577 + 'pages' => 1,
578 + 'page' => $page,
579 + 'per_page' => $per_page,
580 + ];
581 + }
582 +
583 + // Match rows where the email column is set directly OR where the email
584 + // appears as a quoted value inside the serialized form_fields_info blob.
585 + // The initial payment_create() insert never writes the email column —
586 + // only the payment-confirmed callback does — so many rows have the user's
587 + // email exclusively inside form_fields_info.
588 + $ffi_like = '%' . $wpdb->esc_like( '"' . $email . '"' ) . '%';
589 +
590 + $email_where = 'email = %s OR form_fields_info LIKE %s';
591 +
592 + $total = (int) $wpdb->get_var(
593 + $wpdb->prepare(
594 + "SELECT COUNT(id) FROM $table WHERE ( $email_where )" . $type_where,
595 + $email,
596 + $ffi_like
597 + )
598 + );
599 +
600 + $transactions = $wpdb->get_results(
601 + $wpdb->prepare(
602 + "SELECT * FROM $table WHERE ( $email_where )" . $type_where . " ORDER BY payment_date DESC LIMIT %d OFFSET %d",
603 + $email,
604 + $ffi_like,
605 + $per_page,
606 + $offset
607 + )
608 + );
609 +
610 + return [
611 + 'transactions' => $transactions,
612 + 'total' => $total,
613 + 'pages' => $total > 0 ? (int) ceil( $total / $per_page ) : 1,
614 + 'page' => $page,
615 + 'per_page' => $per_page,
616 + ];
617 + }
618 +
619 + /**
620 + * The SQL condition narrowing get_user_transactions_paginated() to a row type.
621 + *
622 + * 'transactions' (or no type) is every row the user owns. Every other type is
623 + * defined by `better_payment/db/user_transactions_type_where` ($where, $type),
624 + * which starts from null: Pro supplies the 'subscriptions' condition for its
625 + * recurring and split payments. A type nobody defines returns null, and the
626 + * caller answers with no rows.
627 + *
628 + * @since 2.4.0
629 + *
630 + * @param string $type Requested row type.
631 + * @return string|null SQL fragment starting with " AND", '' for all rows, null for none.
632 + */
633 + private static function user_transactions_type_where( string $type ) {
634 + if ( '' === $type || 'transactions' === $type ) {
635 + return '';
636 + }
637 +
638 + $where = apply_filters( 'better_payment/db/user_transactions_type_where', null, $type );
639 +
640 + return is_string( $where ) ? $where : null;
641 + }
642 +
643 + /**
355 644 * Delete a transaction
356 645 *
357 646 * @param int $id
358 647 *
@@ -397,8 +686,17 @@
397 686
398 687 /**
399 688 * Get transactions amount
400 689 *
690 + * Lite never calls this; Better Payment Pro <= 1.4.3's AnalyticsModel does, and
691 + * tests/Unit/ProCouplingContractTest.php pins it. Pro 1.4.4 carries its own copy
692 + * (better-payment-pro/includes/Models/AnalyticsModel.php).
693 + *
694 + * @deprecated 2.4.0 A fatal guard only: Pro 1.4.3 is unsupported since
695 + * ProCompatibility::MIN_PRO_VERSION 1.4.4, but a site that updates Lite
696 + * first must not fatal on Pro's Analytics screen. Delete once no
697 + * supported site can run Pro 1.4.3.
698 + *
401 699 * @return array
402 700 * @since 0.0.1
403 701 */
404 702 public function get_transactions_by_statuses($statuses = array(), $payment_date_from = '', $payment_date_to = '', $fetchNull=0) {
@@ -423,17 +721,17 @@
423 721 $statuses = implode(',', $statuses);
424 722
425 723 if(!empty($payment_date_from) && !empty($payment_date_to)){
426 724 $amount = $wpdb->get_results(
427 - "SELECT amount, payment_date FROM $table WHERE ( status IN (" . $statuses . ") " . esc_sql($nullDataQuery) . ") AND payment_date BETWEEN '" . esc_sql($payment_date_from) . "' AND '" . esc_sql($payment_date_to) . "'"
725 + "SELECT " . self::COUNTABLE_AMOUNT_SQL . " AS amount, payment_date, email FROM $table WHERE ( status IN (" . $statuses . ") " . esc_sql($nullDataQuery) . ") AND " . self::COUNTED_ROW_SQL . " AND payment_date BETWEEN '" . esc_sql($payment_date_from) . "' AND '" . esc_sql($payment_date_to) . "'"
428 726 );
429 727 } else if(!empty($payment_date_from) && empty($payment_date_to)){
430 728 $amount = $wpdb->get_results(
431 - "SELECT amount, payment_date FROM $table WHERE ( status IN (" . $statuses . ") " . esc_sql($nullDataQuery) . ") AND payment_date >= '" . esc_sql($payment_date_from) . "'"
729 + "SELECT " . self::COUNTABLE_AMOUNT_SQL . " AS amount, payment_date, email FROM $table WHERE ( status IN (" . $statuses . ") " . esc_sql($nullDataQuery) . ") AND " . self::COUNTED_ROW_SQL . " AND payment_date >= '" . esc_sql($payment_date_from) . "'"
432 730 );
433 731 } else {
434 732 $amount = $wpdb->get_results(
435 - "SELECT amount, payment_date FROM $table WHERE ( status IN (" . $statuses . ") " . esc_sql($nullDataQuery) . " )"
733 + "SELECT " . self::COUNTABLE_AMOUNT_SQL . " AS amount, payment_date, email FROM $table WHERE ( status IN (" . $statuses . ") " . esc_sql($nullDataQuery) . " ) AND " . self::COUNTED_ROW_SQL
436 734 );
437 735 }
438 736
439 737 return $amount;
@@ -441,15 +739,17 @@
441 739
442 740 /**
443 741 * Get transactions amount
444 742 *
445 - * @return array
743 + * Lite's caller, get_transactions_analytics_dashboard(), always passes
744 + * $filtered_transactions and is answered from that array. Without it the answer
745 + * comes from SQL, which only Better Payment Pro <= 1.4.3's AnalyticsModel asks
746 + * for — see transactions_amount_by_statuses_query().
747 + *
748 + * @return mixed
446 749 * @since 0.0.1
447 750 */
448 751 public function get_transactions_amount_by_statuses($statuses = array(), $payment_date_from = '', $payment_date_to = '', $count_or_amount = 'amount', $fetchNull=0, $filtered_transactions = array() ) {
449 - global $wpdb;
450 -
451 - $table = DB::get_table_name();
452 752 $allowed_statuses = $this->allowed_statuses('all', 'v2');
453 753
454 754 $nullDataQuery = $fetchNull ? " OR status IS NULL " : '';
455 755
@@ -468,23 +768,59 @@
468 768 $statuses = implode(',', $statuses);
469 769
470 770 if( is_array($filtered_transactions) && count($filtered_transactions) ){
471 771 $filtered_transactions_by_statuses = array();
772 + $filtered_transactions_amount = 0;
773 +
774 + foreach($filtered_transactions as $filtered_transaction) {
775 + // Helper's counting rule, as the SQL copies below apply it: an unconfirmed pre-2.4.0
776 + // free checkout is no transaction to count, and a confirmed one adds 0.
777 + if ( Helper::is_unconfirmed_legacy_free_checkout( $filtered_transaction ) ) {
778 + continue;
779 + }
472 780
473 - foreach($filtered_transactions as $filtered_transaction) {
474 781 if( in_array($filtered_transaction->status, $statuses_original) ) {
475 782 $filtered_transactions_by_statuses[] = $filtered_transaction->id;
783 + $filtered_transactions_amount += Helper::countable_amount( $filtered_transaction );
476 784 }
477 - }
785 + }
478 786
479 787 if($count_or_amount == 'count'){
480 788 $filtered_transactions_by_statuses_count = count($filtered_transactions_by_statuses);
481 789 return $filtered_transactions_by_statuses_count;
482 790 }
791 +
792 + if($count_or_amount == 'amount'){
793 + return $filtered_transactions_amount;
794 + }
483 795 }
796 +
797 + return $this->transactions_amount_by_statuses_query( $statuses, $nullDataQuery, $payment_date_from, $payment_date_to, $count_or_amount );
798 + }
799 +
800 + /**
801 + * The SQL count or sum behind get_transactions_amount_by_statuses() when no
802 + * filtered transactions are passed.
803 + *
804 + * @deprecated 2.4.0 A fatal guard only, for Better Payment Pro <= 1.4.3 (see
805 + * get_transactions_by_statuses()). Pro 1.4.4 carries its own copy in
806 + * better-payment-pro/includes/Models/AnalyticsModel.php.
807 + *
808 + * @param string $statuses Quoted, comma-separated statuses for IN().
809 + * @param string $nullDataQuery " OR status IS NULL " or ''.
810 + * @param string $payment_date_from Lower payment_date bound.
811 + * @param string $payment_date_to Upper payment_date bound.
812 + * @param string $count_or_amount 'count' or 'amount'.
813 + * @return mixed
814 + */
815 + private function transactions_amount_by_statuses_query( $statuses, $nullDataQuery, $payment_date_from, $payment_date_to, $count_or_amount ) {
816 + global $wpdb;
817 +
818 + $table = DB::get_table_name();
819 +
484 820 if($count_or_amount == 'count') {
485 821 $count = (int) $wpdb->get_var(
486 - "SELECT count(id) FROM $table WHERE ( status IN (" . $statuses . ") " . esc_sql($nullDataQuery) . " )"
822 + "SELECT count(id) FROM $table WHERE ( status IN (" . $statuses . ") " . esc_sql($nullDataQuery) . " ) AND " . self::COUNTED_ROW_SQL
487 823 );
488 824
489 825 return $count;
490 826 }
@@ -490,13 +826,14 @@
490 826 }
491 827
492 828 if(!empty($payment_date_from) && !empty($payment_date_to)){
493 829 $amount = $wpdb->get_var(
494 - "SELECT sum(amount) FROM $table WHERE ( status IN (" . $statuses . ") " . esc_sql($nullDataQuery) . ") AND payment_date BETWEEN '" . esc_sql($payment_date_from) . "' AND '" . esc_sql($payment_date_to) . "'"
830 + "SELECT sum(" . self::COUNTABLE_AMOUNT_SQL . ") FROM $table WHERE ( status IN (" . $statuses . ") " . esc_sql($nullDataQuery) . ") AND " . self::COUNTED_ROW_SQL . " AND payment_date BETWEEN '" . esc_sql($payment_date_from) . "' AND '" . esc_sql($payment_date_to) . "'"
495 831 );
496 832 }else {
497 833 $amount = $wpdb->get_var(
498 - "SELECT sum(amount) FROM $table WHERE ( status IN (" . $statuses . ") " . esc_sql($nullDataQuery) . ")"
834 + // #TODO Amount based on filtered transactions
835 + "SELECT sum(" . self::COUNTABLE_AMOUNT_SQL . ") FROM $table WHERE ( status IN (" . $statuses . ") " . esc_sql($nullDataQuery) . ") AND " . self::COUNTED_ROW_SQL
499 836 );
500 837 }
501 838
502 839 return $amount;
@@ -501,30 +838,18 @@
501 838
502 839 return $amount;
503 840 }
504 841
505 - public static function search_transactions($items, $search_text, $offset, $per_page){
506 - $filtered_items = array();
507 - $search_text = str_replace('%', '', $search_text);
508 -
509 - if(count($items) > 0){
510 - $filtered_items = array_filter($items, function($item) use ($search_text){
511 - $form_fields_info = maybe_unserialize($item->form_fields_info);
512 -
513 - if(
514 - isset($form_fields_info['first_name']) && stripos($form_fields_info['first_name'], $search_text) !== false
515 - || isset($form_fields_info['last_name']) && stripos($form_fields_info['last_name'], $search_text) !== false
516 - || isset($form_fields_info['primary_first_name']) && stripos($form_fields_info['primary_first_name'], $search_text) !== false
517 - || isset($form_fields_info['primary_last_name']) && stripos($form_fields_info['primary_last_name'], $search_text) !== false
518 - || isset($form_fields_info['email']) && stripos($form_fields_info['email'], $search_text) !== false
519 - || isset($form_fields_info['primary_email']) && stripos($form_fields_info['primary_email'], $search_text) !== false
520 - ){
521 - return true;
522 - }
842 + public static function mark_as_completed($id)
843 + {
844 + global $wpdb;
845 + $table = self::get_table_name();
523 846
524 - });
847 + $id = (int) sanitize_text_field($id);
525 848
849 + if ($id > 0) {
850 + return $wpdb->update($table, ['status' => 'Completed'], ['id' => $id]);
526 851 }
527 852
528 - return $filtered_items;
853 + return false;
529 854 }
530 855 }