PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.4
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.4
4.4.7 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 All 138 releases
learnpress / inc / Statistics / InstructorStatisticsProvider.php

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

314 lines 10.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class InstructorStatisticsProvider
4 *
5 * @package LearnPress/Classes/Statistics
6 * @since 4.4.2
7 */
8
9 namespace LearnPress\Statistics;
10
11 use LP_Statistics_DB;
12
13 defined( 'ABSPATH' ) || exit();
14
15 /**
16 * Assembles the Instructors tab payload.
17 *
18 * Composes DashboardStatisticsDB (query layer) and derives KPIs + the
19 * operations widget in PHP from a single performance query — no per-KPI
20 * queries. Pure derivation is split into static helpers so the risk/action
21 * and aggregation logic is unit-testable without a database.
22 *
23 * @since 4.4.2
24 */
25 class InstructorStatisticsProvider {
26 /**
27 * Ceiling for the performance query that also feeds KPI/operations math.
28 * Well above any realistic instructor count; a larger site is logged.
29 */
30 const AGGREGATE_LIMIT = 500;
31
32 /**
33 * @param string $type
34 * @param string $value
35 * @param StatisticsScope|null $scope
36 * @param int $performance_limit Rows kept for the visible table.
37 * @return array
38 */
39 public static function get_statistics( string $type, string $value, ?StatisticsScope $scope = null, int $performance_limit = 10 ): array {
40 $db = DashboardStatisticsDB::getInstance();
41
42 // One query drives the table, KPIs and operations.
43 $rows = $db->get_instructor_performance( $type, $value, $scope, self::AGGREGATE_LIMIT );
44 if ( count( $rows ) >= self::AGGREGATE_LIMIT ) {
45 error_log( 'LP Statistics: instructor count reached the ' . self::AGGREGATE_LIMIT . ' aggregate cap; KPI totals may undercount.' );
46 }
47
48 $total_net_sales = self::get_total_net_sales( $type, $value );
49 $active_instructors = $db->get_instructors_active_in_period( $type, $value, $scope );
50 $pending_rows = $db->get_pending_courses_by_instructor( $scope );
51 $needs_review = array_sum( array_column( $pending_rows, 'pending' ) );
52
53 return array(
54 'kpis' => self::build_kpis( $rows, $total_net_sales, $active_instructors, $needs_review ),
55 'operations' => self::build_operations( $rows, $pending_rows, $needs_review ),
56 'performance' => self::format_performance( array_slice( $rows, 0, max( 1, $performance_limit ) ) ),
57 'watchlist' => $db->get_course_watchlist( $type, $value, $scope, 10 ),
58 );
59 }
60
61 /**
62 * Per-instructor drill-down: sold + enrolled courses merged by course_id,
63 * paginated for the report popup.
64 *
65 * The two source queries are merged in PHP, so the page slice happens after
66 * the merge over a bounded candidate set ( learn-press/statistics/report-max-rows ).
67 *
68 * @param int $instructor_id
69 * @param int $paged 1-based page.
70 * @param int $limit Rows per page.
71 * @param string $search Optional course-title filter.
72 * @return array { instructor_id, rows: [...], total: int, error?: string }
73 */
74 public static function get_report( int $instructor_id, int $paged = 1, int $limit = 20, string $search = '' ): array {
75 $instructor_id = absint( $instructor_id );
76 if ( $instructor_id <= 0 || ! get_user_by( 'id', $instructor_id ) ) {
77 return array(
78 'instructor_id' => $instructor_id,
79 'rows' => array(),
80 'total' => 0,
81 'error' => 'invalid_instructor',
82 );
83 }
84
85 $cap = (int) apply_filters( 'learn-press/statistics/report-max-rows', 2000 );
86 $lp_stats_db = LP_Statistics_DB::getInstance();
87 $sold = $lp_stats_db->get_top_sold_courses_by_instructor( $instructor_id, $cap, $search );
88 $enrolled = $lp_stats_db->get_top_enrolled_courses_by_instructor( $instructor_id, $cap, $search );
89
90 $all = self::merge_report_rows( $sold, $enrolled );
91 $total = count( $all );
92 $paged = max( 1, $paged );
93 $limit = max( 1, $limit );
94 $offset = ( $paged - 1 ) * $limit;
95
96 $rows = array_map(
97 function ( $row ) {
98 $row['revenue_formatted'] = html_entity_decode( learn_press_format_price( $row['revenue'] ) );
99 return $row;
100 },
101 array_slice( $all, $offset, $limit )
102 );
103
104 return array(
105 'instructor_id' => $instructor_id,
106 'rows' => $rows,
107 'total' => $total,
108 );
109 }
110
111 /**
112 * KPI payloads derived from the performance rows + totals. Pure.
113 *
114 * @param array $rows Performance rows (scoped, uncapped-for-math).
115 * @param float $total_net_sales Range net sales across all courses (contribution denominator).
116 * @param int $active_instructors Instructors with an enrollment in range.
117 * @param int $needs_review Pending-review course count.
118 * @return array
119 */
120 public static function build_kpis( array $rows, float $total_net_sales, int $active_instructors, int $needs_review ): array {
121 $instructor_revenue = 0.0;
122 $courses_managed = 0;
123 $students_reached = 0;
124 $rate_sum = 0.0;
125 $rate_count = 0;
126
127 foreach ( $rows as $row ) {
128 $instructor_revenue += (float) ( $row['revenue'] ?? 0 );
129 $courses_managed += (int) ( $row['course_count'] ?? 0 );
130 $students_reached += (int) ( $row['enrolled'] ?? 0 );
131
132 if ( null !== ( $row['completion_rate'] ?? null ) ) {
133 $rate_sum += (float) $row['completion_rate'];
134 ++$rate_count;
135 }
136 }
137
138 $contribution_pct = $total_net_sales > 0
139 ? round( $instructor_revenue / $total_net_sales * 100, 1 )
140 : null;
141 $avg_completion = $rate_count > 0 ? round( $rate_sum / $rate_count, 1 ) : null;
142
143 return array(
144 'active_instructors' => array(
145 'value' => $active_instructors,
146 'total' => count( $rows ),
147 ),
148 'instructor_revenue' => array(
149 'value' => round( $instructor_revenue, 2 ),
150 'formatted' => html_entity_decode( learn_press_format_price( $instructor_revenue ) ),
151 'contribution_pct' => $contribution_pct,
152 ),
153 'courses_managed' => array(
154 'value' => $courses_managed,
155 ),
156 'students_reached' => array(
157 'value' => $students_reached,
158 ),
159 'avg_completion' => array(
160 'value' => $avg_completion,
161 ),
162 'needs_review' => array(
163 'value' => $needs_review,
164 ),
165 );
166 }
167
168 /**
169 * Operations widget highlights from the same rows. Pure.
170 *
171 * @param array $rows Performance rows.
172 * @param array $pending_rows From get_pending_courses_by_instructor(), most pending first.
173 * @param int $needs_review Total pending count.
174 * @return array
175 */
176 public static function build_operations( array $rows, array $pending_rows, int $needs_review ): array {
177 $top_revenue = null;
178 $top_completion = null;
179 $no_new_enrollments = 0;
180
181 foreach ( $rows as $row ) {
182 $revenue = (float) ( $row['revenue'] ?? 0 );
183 if ( null === $top_revenue || $revenue > $top_revenue['value'] ) {
184 $top_revenue = array(
185 'id' => (int) ( $row['instructor_id'] ?? 0 ),
186 'name' => (string) ( $row['instructor_name'] ?? '' ),
187 'value' => $revenue,
188 'value_formatted' => html_entity_decode( learn_press_format_price( $revenue ) ),
189 );
190 }
191
192 $rate = $row['completion_rate'] ?? null;
193 if ( null !== $rate && ( null === $top_completion || $rate > $top_completion['value'] ) ) {
194 $top_completion = array(
195 'id' => (int) ( $row['instructor_id'] ?? 0 ),
196 'name' => (string) ( $row['instructor_name'] ?? '' ),
197 'value' => (float) $rate,
198 );
199 }
200
201 if ( 0 === (int) ( $row['enrolled'] ?? 0 ) ) {
202 ++$no_new_enrollments;
203 }
204 }
205
206 $most_pending = null;
207 if ( ! empty( $pending_rows ) ) {
208 $most_pending = array(
209 'id' => (int) $pending_rows[0]['instructor_id'],
210 'name' => (string) $pending_rows[0]['name'],
211 'value' => (int) $pending_rows[0]['pending'],
212 );
213 }
214
215 return array(
216 'top_revenue' => $top_revenue,
217 'top_completion' => $top_completion,
218 'most_pending' => $most_pending,
219 'no_new_enrollments' => $no_new_enrollments,
220 'review_queue_count' => $needs_review,
221 );
222 }
223
224 /**
225 * Shape performance rows for the table (add formatted revenue). Pure.
226 *
227 * @param array $rows
228 * @return array
229 */
230 public static function format_performance( array $rows ): array {
231 return array_map(
232 function ( $row ) {
233 $revenue = (float) ( $row['revenue'] ?? 0 );
234
235 return array(
236 'instructor_id' => (int) ( $row['instructor_id'] ?? 0 ),
237 'name' => (string) ( $row['instructor_name'] ?? '' ),
238 'courses' => (int) ( $row['course_count'] ?? 0 ),
239 'students' => (int) ( $row['enrolled'] ?? 0 ),
240 'revenue' => $revenue,
241 'revenue_formatted' => html_entity_decode( learn_press_format_price( $revenue ) ),
242 'avg_completion' => $row['completion_rate'] ?? null,
243 );
244 },
245 $rows
246 );
247 }
248
249 /**
250 * Merge sold + enrolled per-course result sets by course_id. Pure.
251 *
252 * @param array $sold From get_top_sold_courses_by_instructor().
253 * @param array $enrolled From get_top_enrolled_courses_by_instructor().
254 * @return array Rows of { course_id, name, sold, revenue, enrolled }.
255 */
256 public static function merge_report_rows( array $sold, array $enrolled ): array {
257 $merged = array();
258
259 foreach ( $sold as $row ) {
260 $course_id = (int) $row->course_id;
261 $merged[ $course_id ] = array(
262 'course_id' => $course_id,
263 'name' => (string) $row->course_name,
264 'sold' => (int) $row->course_count,
265 'revenue' => (float) $row->total_revenue,
266 'enrolled' => 0,
267 );
268 }
269
270 foreach ( $enrolled as $row ) {
271 $course_id = (int) $row->course_id;
272
273 if ( ! isset( $merged[ $course_id ] ) ) {
274 $merged[ $course_id ] = array(
275 'course_id' => $course_id,
276 'name' => (string) $row->course_name,
277 'sold' => 0,
278 'revenue' => 0.0,
279 'enrolled' => 0,
280 );
281 }
282
283 $merged[ $course_id ]['enrolled'] = (int) $row->enrollment_count;
284 }
285
286 usort(
287 $merged,
288 function ( $a, $b ) {
289 return [ $b['revenue'], $b['enrolled'] ] <=> [ $a['revenue'], $a['enrolled'] ];
290 }
291 );
292
293 return array_values( $merged );
294 }
295
296 /**
297 * Range net sales across all completed orders (contribution denominator).
298 *
299 * @param string $type
300 * @param string $value
301 * @return float
302 */
303 private static function get_total_net_sales( string $type, string $value ): float {
304 $rows = LP_Statistics_DB::getInstance()->get_net_sales_data( $type, $value );
305
306 $total = 0.0;
307 foreach ( (array) $rows as $row ) {
308 $total += (float) $row->x_data;
309 }
310
311 return $total;
312 }
313 }
314