PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / trunk
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses vtrunk
4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 4.2.0 4.2.1 4.2.1.1 All 137 releases
learnpress / inc / Statistics / OrderExceptionsProvider.php

OrderExceptionsProvider.php in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses trunk, at inc/Statistics/OrderExceptionsProvider.php

310 lines 9.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class OrderExceptionsProvider
4 *
5 * @package LearnPress/Classes/Statistics
6 * @since 4.4.2
7 */
8
9 namespace LearnPress\Statistics;
10
11 use LP_Database;
12 use LP_Filter;
13 use LP_Statistics_DB;
14
15 defined( 'ABSPATH' ) || exit();
16
17 /**
18 * Recent failed/cancelled order rows for the Orders statistics dashboard.
19 *
20 * LearnPress does not store structured gateway failure reasons, so v1 reports
21 * payment method + order status and exposes a filter for gateway/add-on
22 * enrichment.
23 *
24 * @since 4.4.2
25 */
26 class OrderExceptionsProvider extends LP_Database {
27 /**
28 * @var OrderExceptionsProvider
29 */
30 private static $_instance;
31
32 protected function __construct() {
33 parent::__construct();
34 }
35
36 /**
37 * @return OrderExceptionsProvider
38 */
39 public static function getInstance(): OrderExceptionsProvider {
40 if ( is_null( self::$_instance ) ) {
41 self::$_instance = new self();
42 }
43
44 return self::$_instance;
45 }
46
47 /**
48 * @param string $type
49 * @param string $value
50 * @param string $time_field
51 * @return string
52 */
53 private function time_condition( string $type, string $value, string $time_field ): string {
54 $filter = new LP_Filter();
55 $filter = LP_Statistics_DB::getInstance()->filter_time( $filter, $type, $time_field, $value );
56
57 return $filter->where[0] ?? '';
58 }
59
60 /**
61 * @param StatisticsScope|null $scope
62 * @return string
63 */
64 private function scope_condition( ?StatisticsScope $scope ): string {
65 if ( ! $scope || $scope->is_empty() ) {
66 return '';
67 }
68
69 $filter = new LP_Filter();
70 $scope->apply_to_orders( $filter, 'p.ID' );
71
72 return implode( ' ', $filter->where );
73 }
74
75 /**
76 * WHERE fragment pinning the exception status. Empty/other → both
77 * failed+cancelled (the tab's default); 'failed'/'cancelled' → just that one
78 * (Orders tab deep-link). No '%' in values, safe to interpolate.
79 *
80 * @param string $status
81 * @return string
82 * @since 4.4.2
83 */
84 private function status_condition( string $status ): string {
85 if ( 'failed' === $status ) {
86 return $this->wpdb->prepare( 'p.post_status = %s', LP_ORDER_FAILED_DB );
87 }
88
89 if ( 'cancelled' === $status ) {
90 return $this->wpdb->prepare( 'p.post_status = %s', LP_ORDER_CANCELLED_DB );
91 }
92
93 return $this->wpdb->prepare( 'p.post_status IN ( %s, %s )', LP_ORDER_FAILED_DB, LP_ORDER_CANCELLED_DB );
94 }
95
96 /**
97 * HAVING fragment matching the popup search across course/payment/status.
98 * Empty → no condition. '%' are doubled to survive interpolation into the
99 * outer wpdb::prepare() (see DashboardStatisticsDB::search_condition()).
100 *
101 * @param string $search
102 * @return string
103 * @since 4.4.2
104 */
105 private function search_having( string $search ): string {
106 $search = trim( $search );
107 if ( '' === $search ) {
108 return '';
109 }
110
111 $like = '%' . $this->wpdb->esc_like( $search ) . '%';
112 $fragment = $this->wpdb->prepare(
113 ' HAVING ( course LIKE %s OR payment_method_title LIKE %s OR status LIKE %s )',
114 $like,
115 $like,
116 $like
117 );
118
119 return str_replace( '%', '%%', $fragment );
120 }
121
122 /**
123 * @param string $type
124 * @param string $value
125 * @param StatisticsScope|null $scope
126 * @param int $limit
127 * @param int $offset Row offset for report-popup pagination.
128 * @param string $search Optional course/payment/status filter.
129 * @param string $status Restrict to a single status ( failed|cancelled ).
130 * @return array
131 */
132 public function get_exceptions( string $type, string $value, ?StatisticsScope $scope = null, int $limit = 20, int $offset = 0, string $search = '', string $status = '' ): array {
133 if ( ! $type || ! $value ) {
134 return [];
135 }
136
137 $limit = min( 500, max( 1, $limit ) );
138 $offset = max( 0, $offset );
139 $time = $this->time_condition( $type, $value, 'p.post_date' );
140 $scope_sql = $this->scope_condition( $scope );
141 $status_sql = $this->status_condition( $status );
142 $having = $this->search_having( $search );
143
144 $sql = $this->wpdb->prepare(
145 "SELECT p.ID AS order_id,
146 p.post_date AS date,
147 REPLACE( p.post_status, 'lp-', '' ) AS status,
148 payment_meta.meta_value AS payment_method_title,
149 total_meta.meta_value AS total,
150 user_meta.meta_value AS user_value,
151 GROUP_CONCAT( DISTINCT course_posts.post_title ORDER BY course_posts.post_title SEPARATOR ', ' ) AS course
152 FROM {$this->tb_posts} AS p
153 LEFT JOIN {$this->tb_postmeta} AS payment_meta ON payment_meta.post_id = p.ID AND payment_meta.meta_key = %s
154 LEFT JOIN {$this->tb_postmeta} AS total_meta ON total_meta.post_id = p.ID AND total_meta.meta_key = %s
155 LEFT JOIN {$this->tb_postmeta} AS user_meta ON user_meta.post_id = p.ID AND user_meta.meta_key = %s
156 LEFT JOIN {$this->tb_lp_order_items} AS oi ON oi.order_id = p.ID AND oi.item_type = %s
157 LEFT JOIN {$this->tb_posts} AS course_posts ON course_posts.ID = oi.item_id
158 WHERE p.post_type = %s AND {$status_sql} {$time} {$scope_sql}
159 GROUP BY p.ID, p.post_date, p.post_status, payment_meta.meta_value, total_meta.meta_value, user_meta.meta_value
160 {$having}
161 ORDER BY p.post_date DESC, p.ID DESC
162 LIMIT %d OFFSET %d",
163 '_payment_method_title',
164 '_order_total',
165 '_user_id',
166 LP_COURSE_CPT,
167 LP_ORDER_CPT,
168 $limit,
169 $offset
170 );
171
172 $rows = $this->wpdb->get_results( $sql );
173 if ( ! is_array( $rows ) ) {
174 return [];
175 }
176
177 return array_map( [ $this, 'format_exception_row' ], $rows );
178 }
179
180 /**
181 * Total exception rows matching get_exceptions()'s filters — the row total
182 * for report-popup pagination.
183 *
184 * @param string $type
185 * @param string $value
186 * @param StatisticsScope|null $scope
187 * @param string $search
188 * @param string $status
189 * @return int
190 * @since 4.4.2
191 */
192 public function count_exceptions( string $type, string $value, ?StatisticsScope $scope = null, string $search = '', string $status = '' ): int {
193 if ( ! $type || ! $value ) {
194 return 0;
195 }
196
197 $time = $this->time_condition( $type, $value, 'p.post_date' );
198 $scope_sql = $this->scope_condition( $scope );
199 $status_sql = $this->status_condition( $status );
200 $having = $this->search_having( $search );
201
202 $sql = $this->wpdb->prepare(
203 "SELECT COUNT(*) FROM (
204 SELECT p.ID,
205 REPLACE( p.post_status, 'lp-', '' ) AS status,
206 payment_meta.meta_value AS payment_method_title,
207 GROUP_CONCAT( DISTINCT course_posts.post_title SEPARATOR ', ' ) AS course
208 FROM {$this->tb_posts} AS p
209 LEFT JOIN {$this->tb_postmeta} AS payment_meta ON payment_meta.post_id = p.ID AND payment_meta.meta_key = %s
210 LEFT JOIN {$this->tb_lp_order_items} AS oi ON oi.order_id = p.ID AND oi.item_type = %s
211 LEFT JOIN {$this->tb_posts} AS course_posts ON course_posts.ID = oi.item_id
212 WHERE p.post_type = %s AND {$status_sql} {$time} {$scope_sql}
213 GROUP BY p.ID, p.post_status, payment_meta.meta_value
214 {$having}
215 ) AS t",
216 '_payment_method_title',
217 LP_COURSE_CPT,
218 LP_ORDER_CPT
219 );
220
221 return (int) $this->wpdb->get_var( $sql );
222 }
223
224 /**
225 * Pure issue text builder.
226 *
227 * @param string $payment_method
228 * @param string $status
229 * @return string
230 */
231 public static function issue_text( string $payment_method, string $status ): string {
232 $payment_method = '' !== $payment_method ? $payment_method : __( 'Unknown payment method', 'learnpress' );
233
234 return sprintf( '%1$s - %2$s', $payment_method, $status );
235 }
236
237 /**
238 * Pure severity bucket mapper.
239 *
240 * @param string $status failed|cancelled.
241 * @param float $total
242 * @return string high|medium|low
243 */
244 public static function severity( string $status, float $total ): string {
245 if ( 'failed' === $status && $total > 0 ) {
246 return 'high';
247 }
248
249 if ( 'cancelled' === $status && $total > 0 ) {
250 return 'medium';
251 }
252
253 return 'low';
254 }
255
256 /**
257 * @param object $row
258 * @return array
259 */
260 private function format_exception_row( object $row ): array {
261 $order_id = (int) $row->order_id;
262 $status = sanitize_key( (string) $row->status );
263 $total = (float) ( $row->total ?? 0 );
264 $payment_method = sanitize_text_field( (string) ( $row->payment_method_title ?? '' ) );
265
266 $defaults = [
267 'order_id' => $order_id,
268 'edit_link' => get_edit_post_link( $order_id, 'raw' ),
269 'student' => $this->get_student_name( $row->user_value ?? '' ),
270 'course' => (string) ( $row->course ?? '' ),
271 'status' => $status,
272 'issue' => self::issue_text( $payment_method, $status ),
273 'date' => mysql2date( 'Y-m-d H:i:s', (string) $row->date ),
274 'severity' => self::severity( $status, $total ),
275 ];
276
277 $filtered = apply_filters( 'learn-press/statistics/order-exception-data', $defaults, $order_id );
278 $filtered = wp_parse_args( (array) $filtered, $defaults );
279
280 return [
281 'order_id' => (int) $filtered['order_id'],
282 'edit_link' => esc_url_raw( (string) $filtered['edit_link'] ),
283 'student' => sanitize_text_field( (string) $filtered['student'] ),
284 'course' => sanitize_text_field( (string) $filtered['course'] ),
285 'status' => sanitize_key( (string) $filtered['status'] ),
286 'issue' => sanitize_text_field( (string) $filtered['issue'] ),
287 'date' => sanitize_text_field( (string) $filtered['date'] ),
288 'severity' => sanitize_key( (string) $filtered['severity'] ),
289 ];
290 }
291
292 /**
293 * @param mixed $raw_user_value
294 * @return string
295 */
296 private function get_student_name( $raw_user_value ): string {
297 $user_ids = maybe_unserialize( $raw_user_value );
298 $user_ids = is_array( $user_ids ) ? $user_ids : [ $user_ids ];
299 $user_id = absint( reset( $user_ids ) );
300
301 if ( ! $user_id ) {
302 return '';
303 }
304
305 $user = get_userdata( $user_id );
306
307 return $user ? (string) $user->display_name : '';
308 }
309 }
310