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
better-payment / includes / Admin / DB.php

DB.php in Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More 2.4.1, at includes/Admin/DB.php

856 lines 35.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Better_Payment\Lite\Admin;
4
5 use Better_Payment\Lite\Classes\Helper;
6
7 /**
8 * Exit if accessed directly
9 */
10 if (!defined('ABSPATH')) {
11 exit;
12 }
13
14 /**
15 * This class responsible for database work
16 * using wordpress functionality
17 * get_option and update_option.
18 *
19 * @since 0.0.1
20 */
21 class DB {
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
31 public static function get_table_name() {
32 global $wpdb;
33
34 $table = "{$wpdb->prefix}better_payment";
35 return $table;
36 }
37 /**
38 * Get all default settings value.
39 *
40 * @param string $name
41 * @return array
42 * @since 0.0.1
43 */
44 public static function default_settings() {
45 return apply_filters('better_payment_option_default_settings', array(
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',
53
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',
63
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',
72
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',
112 ));
113 }
114 /**
115 * Get all settings value from options table.
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 *
120 * @param string $name
121 * @return mixed
122 * @since 0.0.1
123 */
124 public static function get_settings($name = '')
125 {
126 $settings = get_option('better_payment_settings', true);
127 $default = self::default_settings();
128
129 $settings = wp_parse_args($settings, $default);
130
131 if (!empty($name) && isset($settings[$name])) {
132 return $settings[$name];
133 }
134
135 if (!empty($name) && !isset($settings[$name]) && isset($default[$name])) {
136 return $default[$name];
137 }
138
139 if (!empty($name) && !isset($settings[$name]) && !isset($default[$name])) {
140 return '';
141 }
142
143 return is_array($settings) ? $settings : [];
144 }
145 /**
146 * Update settings
147 * @param array $value
148 * @return boolean
149 * @since 0.0.1
150 */
151 public static function update_settings($value, $key = '')
152 {
153 if (!empty($key)) {
154 return update_option($key, $value);
155 }
156 return update_option('better_payment_settings', $value);
157 }
158
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 /**
192 * Get all transactions from better payment table.
193 *
194 * @param array|string $args Filters, as wp_parse_args() reads them (every caller passes an array).
195 * @return array
196 * @since 0.0.1
197 */
198 public static function get_transactions($args = [], $count_only=0, $version = 'v1', $fetchNull = 0, $transaction_type = '')
199 {
200 global $wpdb;
201 $items = '';
202
203 $defaults = array(
204 'search_text' => '',
205 'payment_date_from' => '',
206 'payment_date_to' => '',
207 'order_by' => 'payment_date',
208 'order' => 'DESC',
209 'currency' => 'all',
210 'source' => '',
211 'paged' => 1,
212 'per_page' => 20,
213 'offset' => 0,
214 'status' => 'all',
215 );
216
217 $allowed_order_by = array(
218 'id',
219 'payment_date',
220 'email',
221 'amount'
222 );
223
224 $allowed_order = array(
225 'DESC',
226 'ASC'
227 );
228
229 $better_payment_db_obj = new DB();
230 $better_payment_helper_obj = new Helper();
231
232 $allowed_statuses = $better_payment_db_obj->allowed_statuses('all', 'v2');
233 $allowed_statuses_types = $better_payment_db_obj->allowed_statuses_types('v2');
234
235 $args = wp_parse_args($args, $defaults);
236 $table = self::get_table_name();
237
238 $search_text = sanitize_text_field($args['search_text']);
239 $search_text = empty($search_text) ? '' : "%$search_text%";
240
241 $order_by = in_array($args['order_by'], $allowed_order_by) ? sanitize_text_field($args['order_by']) : $defaults['order_by'];
242 $order = in_array($args['order'], $allowed_order) ? sanitize_text_field($args['order']) : $defaults['order'];
243 $offset = $args['offset'] > 0 ? intval($args['offset']) : $defaults['offset'];
244 $per_page = $args['per_page'] > 0 ? intval($args['per_page']) : $defaults['per_page'];
245
246 if($args['status'] == 'null') {
247 $status = sanitize_text_field($args['status']);
248 }else {
249 $allowed_statuses_by_version = ($version == 'v1') ? $allowed_statuses : $allowed_statuses_types;
250 $status = in_array($args['status'], $allowed_statuses_by_version) ? sanitize_text_field($args['status']) : $defaults['status'];
251 }
252
253 $statuses_by_transaction_type = array();
254
255 if( !empty($transaction_type) ){
256 $statuses_by_types = $better_payment_helper_obj->transaction_statuses_with_type_v2();
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 }
270 }
271
272 $whereQuery = '';
273
274 $valid_search_text = $search_text != '' && strlen($search_text) >= 2;
275 if($valid_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 );
277 }
278
279 if( $args['source'] != '' ){
280 $whereQuery .= $wpdb->prepare(" AND source = %s", $args['source']);
281 }
282
283 if( $args['currency'] != 'all' ){
284 $whereQuery .= $wpdb->prepare(" AND currency = %s", $args['currency']);
285 }
286
287 if( $status != 'all' ){
288 if($status == 'null'){
289 $whereQuery .= esc_sql(" AND status is NULL");
290 }else {
291 //version 2
292 if('v1' === $version){
293 $whereQuery .= $wpdb->prepare( " AND status = %s OR status = %s", $status, strtolower($status) );
294 } else {
295
296 if($transaction_type){
297 $statuses_by_type = $statuses_by_transaction_type;
298 }else {
299 $statuses_by_type = $better_payment_db_obj->allowed_statuses($status, 'v2');
300 }
301
302 $statuses = array_map(function($status) {
303 return "'" . esc_sql($status) . "'";
304 }, $statuses_by_type);
305
306 $statuses = implode(',', $statuses);
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 }
327 }
328 }
329 }
330
331 if( $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]);
334 }
335
336 if($count_only === 1){
337 if($valid_search_text){
338 $items = $wpdb->get_var(
339 "SELECT count(id) FROM $table WHERE 1
340 $whereQuery
341 ORDER BY $order_by $order
342 "
343 );
344 }else {
345 $items = $wpdb->get_var(
346 $wpdb->prepare(
347 "SELECT count(id) FROM $table WHERE 1
348 $whereQuery
349 ORDER BY $order_by $order
350 LIMIT %d, %d
351 ",
352 $offset,
353 $per_page
354 )
355 );
356 }
357
358 }else {
359 $items = $wpdb->get_results(
360 $wpdb->prepare(
361 "SELECT * FROM $table WHERE 1
362 $whereQuery
363 ORDER BY $order_by $order
364 LIMIT %d, %d
365 ",
366 $offset,
367 $per_page
368 )
369 );
370
371 }
372
373 return $items;
374 }
375
376 /**
377 * Get the count of total transactions
378 *
379 * @return int
380 * @since 0.0.1
381 */
382 public static function get_transaction_count($args = '', $version = 'v1', $fetchNull = 0, $transaction_type = '')
383 {
384 global $wpdb;
385 $table = self::get_table_name();
386
387 $count = 0;
388
389 if( !empty($transaction_type) ){
390 $count = self::get_transactions($args, 1, $version, $fetchNull, $transaction_type);
391 return $count;
392 }
393
394 if($args !== ''){
395 $count = self::get_transactions($args, 1, $version, $fetchNull);
396 }else {
397 $count = $wpdb->get_var(
398 "SELECT count(id) FROM $table"
399 );
400 }
401
402 return $count;
403 }
404
405 /**
406 * Get transactions analytics
407 *
408 * @return array<string, mixed>
409 * @since 0.0.1
410 */
411 public static function get_transactions_analytics_dashboard($filtered_transactions = array())
412 {
413 global $wpdb;
414 $transaction_analytics = [];
415
416 $table = self::get_table_name();
417 $better_payment_db_obj = new DB();
418
419 $total_transactions_count = 0;
420 $completed_transactions_count = 0;
421 $incomplete_transactions_count = 0;
422 $refunded_transactions_count = 0;
423
424 $total_transactions_amount = 0;
425 $completed_transactions_amount = 0;
426 $incomplete_transactions_amount = 0;
427 $refunded_transactions_amount = 0;
428
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 }
445
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;
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
458 return $transaction_analytics;
459 }
460
461 /**
462 * Get transaction details from better payment table.
463 *
464 * @param string $args
465 * @return object
466 * @since 0.0.1
467 */
468 public static function get_transaction($id)
469 {
470 global $wpdb;
471
472 $table = self::get_table_name();
473 $transaction_id = (int) sanitize_text_field($id);
474
475 $item = $wpdb->get_row(
476 $wpdb->prepare(
477 "SELECT * FROM $table WHERE id = %d",
478 $transaction_id
479 )
480 );
481
482 return $item;
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 . '"' ) . '%';
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
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 /**
644 * Delete a transaction
645 *
646 * @param int $id
647 *
648 * @return int|boolean
649 * @since 0.0.1
650 */
651 public static function delete_transaction($id)
652 {
653 global $wpdb;
654 $table = self::get_table_name();
655
656 return $wpdb->delete(
657 $table,
658 ['id' => $id],
659 ['%d']
660 );
661 }
662
663 /**
664 * Allowed transaction statuses
665 *
666 * @since 0.0.1
667 */
668 public function allowed_statuses($type = 'all', $version = 'v1'){
669 $helperObj = new Helper();
670 $statuses = $helperObj->get_statuses_by_transaction_type($type, $version);
671
672 return $statuses;
673 }
674
675 /**
676 * Allowed transaction statuses
677 *
678 * @since 0.0.1
679 */
680 public function allowed_statuses_types($version = 'v1'){
681 $helperObj = new Helper();
682 $statuses = $helperObj->get_transaction_types($version);
683
684 return $statuses;
685 }
686
687 /**
688 * Get transactions amount
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 *
699 * @return array
700 * @since 0.0.1
701 */
702 public function get_transactions_by_statuses($statuses = array(), $payment_date_from = '', $payment_date_to = '', $fetchNull=0) {
703 global $wpdb;
704
705 $table = DB::get_table_name();
706 $allowed_statuses = $this->allowed_statuses('all', 'v2');
707
708 $nullDataQuery = $fetchNull ? " OR status IS NULL " : '';
709
710 $statuses = is_array($statuses) && count($statuses) ? $statuses : $allowed_statuses;
711
712 #ToDo: Consider currency while calculating total amount
713 $statuses = array_map(function($status) use ($allowed_statuses) {
714 if(!in_array($status, $allowed_statuses)) {
715 return false;
716 }
717
718 return "'" . esc_sql($status) . "'";
719 }, $statuses);
720
721 $statuses = implode(',', $statuses);
722
723 if(!empty($payment_date_from) && !empty($payment_date_to)){
724 $amount = $wpdb->get_results(
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) . "'"
726 );
727 } else if(!empty($payment_date_from) && empty($payment_date_to)){
728 $amount = $wpdb->get_results(
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) . "'"
730 );
731 } else {
732 $amount = $wpdb->get_results(
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
734 );
735 }
736
737 return $amount;
738 }
739
740 /**
741 * Get transactions amount
742 *
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
749 * @since 0.0.1
750 */
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() ) {
752 $allowed_statuses = $this->allowed_statuses('all', 'v2');
753
754 $nullDataQuery = $fetchNull ? " OR status IS NULL " : '';
755
756 $statuses = is_array($statuses) && count($statuses) ? $statuses : $allowed_statuses;
757 $statuses_original = $statuses;
758
759 #ToDo: Consider currency while calculating total amount
760 $statuses = array_map(function($status) use ($allowed_statuses) {
761 if(!in_array($status, $allowed_statuses)) {
762 return false;
763 }
764
765 return "'" . esc_sql($status) . "'";
766 }, $statuses);
767
768 $statuses = implode(',', $statuses);
769
770 if( is_array($filtered_transactions) && count($filtered_transactions) ){
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 }
780
781 if( in_array($filtered_transaction->status, $statuses_original) ) {
782 $filtered_transactions_by_statuses[] = $filtered_transaction->id;
783 $filtered_transactions_amount += Helper::countable_amount( $filtered_transaction );
784 }
785 }
786
787 if($count_or_amount == 'count'){
788 $filtered_transactions_by_statuses_count = count($filtered_transactions_by_statuses);
789 return $filtered_transactions_by_statuses_count;
790 }
791
792 if($count_or_amount == 'amount'){
793 return $filtered_transactions_amount;
794 }
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
820 if($count_or_amount == 'count') {
821 $count = (int) $wpdb->get_var(
822 "SELECT count(id) FROM $table WHERE ( status IN (" . $statuses . ") " . esc_sql($nullDataQuery) . " ) AND " . self::COUNTED_ROW_SQL
823 );
824
825 return $count;
826 }
827
828 if(!empty($payment_date_from) && !empty($payment_date_to)){
829 $amount = $wpdb->get_var(
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) . "'"
831 );
832 }else {
833 $amount = $wpdb->get_var(
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
836 );
837 }
838
839 return $amount;
840 }
841
842 public static function mark_as_completed($id)
843 {
844 global $wpdb;
845 $table = self::get_table_name();
846
847 $id = (int) sanitize_text_field($id);
848
849 if ($id > 0) {
850 return $wpdb->update($table, ['status' => 'Completed'], ['id' => $id]);
851 }
852
853 return false;
854 }
855 }
856