Diff: STRATO-apps/wordpress_03/app/wp-content/plugins/tutor/ecommerce/HooksHandler.php
Keine Baseline-Datei – Diff nur gegen leer.
1
-
1
+
<?php
2
+
/**
3
+
* Handle ecommerce hooks
4
+
*
5
+
* @package Tutor\Ecommerce
6
+
* @author Themeum <support@themeum.com>
7
+
* @link https://themeum.com
8
+
* @since 3.0.0
9
+
*/
10
+
11
+
namespace Tutor\Ecommerce;
12
+
13
+
use TUTOR\Input;
14
+
use TUTOR\Course;
15
+
use TUTOR\Earnings;
16
+
use Tutor\Models\CartModel;
17
+
use Tutor\Models\OrderModel;
18
+
use Tutor\Helpers\QueryHelper;
19
+
use Tutor\Models\OrderMetaModel;
20
+
use Tutor\Models\OrderActivitiesModel;
21
+
use TutorPro\CourseBundle\Models\BundleModel;
22
+
use TutorPro\CourseBundle\CustomPosts\CourseBundle;
23
+
24
+
/**
25
+
* Handle custom hooks
26
+
*/
27
+
class HooksHandler {
28
+
29
+
/**
30
+
* OrderModel
31
+
*
32
+
* @since 3.0.0
33
+
*
34
+
* @var OrderModel
35
+
*/
36
+
private $order_model;
37
+
38
+
/**
39
+
* OrderActivitiesModel
40
+
*
41
+
* @since 3.0.0
42
+
*
43
+
* @var OrderActivitiesModel
44
+
*/
45
+
private $order_activities_model;
46
+
47
+
/**
48
+
* Coupon controller instance
49
+
*
50
+
* @since 3.5.0
51
+
*
52
+
* @var CouponController
53
+
*/
54
+
private $coupon_ctrl;
55
+
56
+
/**
57
+
* Register hooks & resolve props
58
+
*
59
+
* @since 3.0.0
60
+
*/
61
+
public function __construct() {
62
+
$this->order_activities_model = new OrderActivitiesModel();
63
+
$this->order_model = new OrderModel();
64
+
$this->coupon_ctrl = new CouponController( false );
65
+
66
+
// Register hooks.
67
+
add_filter( 'tutor_course_sell_by', array( $this, 'alter_course_sell_by' ) );
68
+
add_filter( 'get_tutor_course_price', array( $this, 'alter_course_price' ), 10, 2 );
69
+
70
+
// Order hooks.
71
+
add_action( 'tutor_order_payment_updated', array( $this, 'handle_payment_updated_webhook' ) );
72
+
73
+
add_action( 'tutor_order_payment_status_changed', array( $this, 'handle_payment_status_changed' ), 10, 3 );
74
+
75
+
add_action( 'tutor_order_placement_success', array( $this, 'handle_order_placement_success' ) );
76
+
77
+
/**
78
+
* Clear order menu badge count
79
+
*
80
+
* @since 3.0.0
81
+
*/
82
+
add_action( 'tutor_order_placed', array( $this, 'clear_order_badge_count' ) );
83
+
add_action( 'tutor_order_payment_status_changed', array( $this, 'clear_order_badge_count' ) );
84
+
add_action( 'tutor_before_order_bulk_action', array( $this, 'clear_order_badge_count' ) );
85
+
add_filter( 'tutor_before_order_create', array( $this, 'update_order_data' ) );
86
+
add_action( 'tutor_order_placed', array( $this, 'handle_free_checkout' ) );
87
+
add_filter( 'tutor_redirect_url_after_checkout', array( $this, 'redirect_to_the_course' ), 10, 3 );
88
+
89
+
/**
90
+
* Store customer billing information for each order.
91
+
*
92
+
* @since 3.5.0
93
+
*/
94
+
add_action( 'tutor_order_placed', array( $this, 'store_billing_address_for_order' ) );
95
+
add_action( 'tutor_order_updated', array( $this, 'store_billing_address_for_order' ) );
96
+
}
97
+
98
+
/**
99
+
* Clear order menu badge count
100
+
*
101
+
* @since 3.0.0
102
+
*
103
+
* @return void
104
+
*/
105
+
public function clear_order_badge_count() {
106
+
delete_transient( OrderModel::TRANSIENT_ORDER_BADGE_COUNT );
107
+
}
108
+
109
+
/**
110
+
* Store order activity before bulk action.
111
+
*
112
+
* @since 3.0.0
113
+
*
114
+
* @param string $bulk_action The bulk action being performed.
115
+
* @param array $bulk_ids The IDs of the orders being acted upon.
116
+
*
117
+
* @return void
118
+
*/
119
+
public function after_order_bulk_action( $bulk_action, $bulk_ids ) {
120
+
$order_status = $this->order_model->get_order_status_by_payment_status( $bulk_action );
121
+
122
+
$cancel_reason = Input::post( 'cancel_reason', '' );
123
+
foreach ( $bulk_ids as $order_id ) {
124
+
try {
125
+
$this->manage_earnings_and_enrollments( $order_status, $order_id );
126
+
$data = (object) array(
127
+
'order_id' => $order_id,
128
+
'meta_key' => $this->order_activities_model::META_KEY_HISTORY,
129
+
'meta_value' => "Order mark as {$bulk_action} {$cancel_reason}",
130
+
);
131
+
$this->order_activities_model->add_order_meta( $data );
132
+
} catch ( \Throwable $th ) {
133
+
// Log message with line & file.
134
+
error_log( $th->getMessage() . ' in ' . $th->getFile() . ' at line ' . $th->getLine() );
135
+
}
136
+
}
137
+
}
138
+
139
+
/**
140
+
* Alter course sell by value
141
+
*
142
+
* @since 3.0.0
143
+
*
144
+
* @param mixed $sell_by Default sell by.
145
+
*
146
+
* @return mixed
147
+
*/
148
+
public function alter_course_sell_by( $sell_by ) {
149
+
if ( tutor_utils()->is_monetize_by_tutor() ) {
150
+
$sell_by = Ecommerce::MONETIZE_BY;
151
+
}
152
+
153
+
return $sell_by;
154
+
}
155
+
156
+
/**
157
+
* Alter course price to show price on the course
158
+
* entry box
159
+
*
160
+
* @since 3.0.0
161
+
*
162
+
* @param mixed $price Course price.
163
+
* @param int $course_id Course id.
164
+
*
165
+
* @return mixed
166
+
*/
167
+
public function alter_course_price( $price, $course_id ) {
168
+
$price_type = tutor_utils()->price_type( $course_id );
169
+
if ( tutor_utils()->is_monetize_by_tutor() && Course::PRICE_TYPE_PAID === $price_type ) {
170
+
$price = tutor_get_course_formatted_price_html( $course_id, false );
171
+
}
172
+
173
+
return $price;
174
+
}
175
+
176
+
/**
177
+
* Handle payment updated webhook
178
+
*
179
+
* @since 3.0.0
180
+
*
181
+
* @param object $res Response data.
182
+
* {order_id, transaction_id, payment_status, payment_method, redirectUrl}.
183
+
*
184
+
* @return void
185
+
*/
186
+
public function handle_payment_updated_webhook( $res ) {
187
+
$order_id = $res->id;
188
+
$new_payment_status = $res->payment_status;
189
+
$transaction_id = $res->transaction_id;
190
+
191
+
$order_details = $this->order_model->get_order_by_id( $order_id );
192
+
if ( $order_details ) {
193
+
$prev_payment_status = $order_details->payment_status;
194
+
195
+
$order_data = array(
196
+
'order_status' => $order_details->order_status,
197
+
'payment_status' => $new_payment_status,
198
+
'payment_method' => $res->payment_method,
199
+
'payment_payloads' => $res->payment_payload,
200
+
'transaction_id' => $transaction_id,
201
+
'earnings' => $res->earnings,
202
+
'fees' => $res->fees,
203
+
'updated_at_gmt' => current_time( 'mysql', true ),
204
+
);
205
+
206
+
switch ( $new_payment_status ) {
207
+
case $this->order_model::PAYMENT_PAID:
208
+
$order_data['order_status'] = $this->order_model::ORDER_COMPLETED;
209
+
break;
210
+
case $this->order_model::PAYMENT_FAILED:
211
+
case $this->order_model::PAYMENT_REFUNDED:
212
+
$order_data['order_status'] = $this->order_model::ORDER_CANCELLED;
213
+
break;
214
+
}
215
+
216
+
$update = $this->order_model->update_order( $order_id, $order_data );
217
+
if ( $update ) {
218
+
// Provide hook after update order.
219
+
do_action( 'tutor_order_payment_status_changed', $order_id, $prev_payment_status, $new_payment_status );
220
+
}
221
+
}
222
+
}
223
+
224
+
/**
225
+
* Update enrollment & earnings based on payment status
226
+
*
227
+
* @since 3.0.0
228
+
*
229
+
* @param int $order_id Order id.
230
+
* @param string $prev_payment_status previous payment status.
231
+
* @param string $new_payment_status new payment status.
232
+
*
233
+
* @return void
234
+
*/
235
+
public function handle_payment_status_changed( $order_id, $prev_payment_status, $new_payment_status ) {
236
+
237
+
$order_status = $this->order_model->get_order_status_by_payment_status( $new_payment_status );
238
+
239
+
$cancel_reason = Input::post( 'cancel_reason' );
240
+
$remove_enrollment = Input::post( 'is_remove_enrolment', false, Input::TYPE_BOOL );
241
+
242
+
// Store activity.
243
+
$data = (object) array(
244
+
'order_id' => $order_id,
245
+
'meta_key' => $this->order_activities_model::META_KEY_HISTORY,
246
+
'meta_value' => 'Order marked as ' . $new_payment_status,
247
+
);
248
+
249
+
if ( $cancel_reason ) {
250
+
$meta_value = array(
251
+
'message' => 'Order marked as ' . $new_payment_status,
252
+
'cancel_reason' => $cancel_reason,
253
+
);
254
+
$data->meta_value = json_encode( $meta_value );
255
+
}
256
+
257
+
$this->order_activities_model->add_order_meta( $data );
258
+
259
+
if ( $remove_enrollment ) {
260
+
$order_status = OrderModel::ORDER_CANCELLED;
261
+
}
262
+
263
+
$this->manage_earnings_and_enrollments( $order_status, $order_id );
264
+
265
+
// Store coupon usage.
266
+
$this->coupon_ctrl->store_coupon_usage( $order_id );
267
+
}
268
+
269
+
/**
270
+
* Handle new order placement
271
+
*
272
+
* Clear cart items, managing enrollment & earnings
273
+
*
274
+
* @since 3.0.0
275
+
*
276
+
* @param int $order_id Order id.
277
+
*
278
+
* @return void
279
+
*/
280
+
public function handle_order_placement_success( int $order_id ) {
281
+
$order_data = $this->order_model->get_order_by_id( $order_id );
282
+
if ( $order_data ) {
283
+
$user_id = $order_data->student->id;
284
+
285
+
( new CartModel() )->clear_user_cart( $user_id );
286
+
287
+
// Manage enrollment & earnings.
288
+
$order = ( new OrderModel() )->get_order_by_id( $order_id );
289
+
$payment_status = $order->payment_status;
290
+
291
+
$order_status = $this->order_model->get_order_status_by_payment_status( $payment_status );
292
+
293
+
$this->manage_earnings_and_enrollments( $order_status, $order_id );
294
+
}
295
+
}
296
+
297
+
/**
298
+
* Check if order is bundle order
299
+
*
300
+
* @since 3.0.0
301
+
*
302
+
* @param object $order order object.
303
+
* @param int $object_id object id.
304
+
*
305
+
* @return boolean
306
+
*/
307
+
private function is_bundle_order( $order, $object_id ) {
308
+
return tutor_utils()->is_addon_enabled( 'course-bundle' )
309
+
&& in_array( $order->order_type, array( $this->order_model::TYPE_SINGLE_ORDER, $this->order_model::TYPE_SUBSCRIPTION ), true )
310
+
&& 'course-bundle' === get_post_type( $object_id );
311
+
}
312
+
313
+
/**
314
+
* Manage earnings after order bulk action
315
+
*
316
+
* @since 3.0.0
317
+
*
318
+
* @param string $order_status Order status.
319
+
* @param int $order_id Order ID.
320
+
*
321
+
* @return void
322
+
*/
323
+
public function manage_earnings_and_enrollments( string $order_status, int $order_id ) {
324
+
$earnings = Earnings::get_instance();
325
+
$order = $this->order_model->get_order_by_id( $order_id );
326
+
$student_id = $order->student->id;
327
+
328
+
$enrollment_status = ( OrderModel::ORDER_COMPLETED === $order_status ? 'completed' : ( OrderModel::ORDER_INCOMPLETE === $order->order_status ? 'pending' : 'cancel' ) );
329
+
330
+
foreach ( $order->items as $item ) {
331
+
$object_id = $item->id; // It could be course/bundle/plan id.
332
+
$is_gift_item = apply_filters( 'tutor_is_gift_item', false, $item->primary_id );
333
+
if ( $is_gift_item ) {
334
+
continue;
335
+
}
336
+
337
+
if ( $this->order_model::TYPE_SINGLE_ORDER !== $order->order_type ) {
338
+
/**
339
+
* Do not process enrollment for membership plan.
340
+
*
341
+
* @since 3.2.0
342
+
*/
343
+
$plan_info = apply_filters( 'tutor_get_plan_info', null, $object_id );
344
+
if ( $plan_info && isset( $plan_info->is_membership_plan ) && $plan_info->is_membership_plan ) {
345
+
continue;
346
+
} else {
347
+
$object_id = apply_filters( 'tutor_subscription_course_by_plan', $item->id, $order );
348
+
}
349
+
350
+
/**
351
+
* Do not process enrollment for subscription order refund.
352
+
* It will be handled by subscription controller's handle_order_refund method.
353
+
*
354
+
* @since 3.3.0
355
+
*/
356
+
if ( Input::has( 'is_cancel_subscription' ) ) {
357
+
continue;
358
+
}
359
+
}
360
+
361
+
$has_enrollment = tutor_utils()->is_enrolled( $object_id, $student_id, false );
362
+
if ( $has_enrollment ) {
363
+
// Update enrollment status based on order status.
364
+
$update = tutor_utils()->update_enrollments( $enrollment_status, array( $has_enrollment->ID ) );
365
+
if ( $update ) {
366
+
if ( $this->is_bundle_order( $order, $object_id ) && $this->order_model->is_single_order( $order ) ) {
367
+
if ( 'completed' === $enrollment_status ) {
368
+
BundleModel::enroll_to_bundle_courses( $object_id, $student_id );
369
+
} else {
370
+
BundleModel::disenroll_from_bundle_courses( $object_id, $student_id );
371
+
}
372
+
}
373
+
374
+
/**
375
+
* For subscription, renewal no need to update order id.
376
+
*/
377
+
if ( $this->order_model->is_single_order( $order ) ) {
378
+
update_post_meta( $has_enrollment->ID, '_tutor_enrolled_by_order_id', $order_id );
379
+
380
+
/**
381
+
* Update enrollment expiry date if it is set in a course.
382
+
*/
383
+
if ( tutor()->course_post_type === get_post_type( $object_id ) ) {
384
+
$is_set_enrollment_expiry = (int) get_tutor_course_settings( $object_id, 'enrollment_expiry' );
385
+
$enrollment_expiry_enabled = (bool) get_tutor_option( 'enrollment_expiry_enabled' );
386
+
if ( $enrollment_expiry_enabled && $is_set_enrollment_expiry ) {
387
+
global $wpdb;
388
+
QueryHelper::update(
389
+
$wpdb->posts,
390
+
array(
391
+
'post_date' => current_time( 'mysql' ),
392
+
'post_date_gmt' => current_time( 'mysql', true ),
393
+
),
394
+
array(
395
+
'ID' => $has_enrollment->ID,
396
+
'post_type' => tutor()->enrollment_post_type,
397
+
)
398
+
);
399
+
}
400
+
}
401
+
402
+
if ( OrderModel::ORDER_COMPLETED === $order_status ) {
403
+
do_action( 'tutor_after_enrolled', $object_id, $student_id, $has_enrollment->ID );
404
+
}
405
+
}
406
+
407
+
if ( 'completed' === $enrollment_status ) {
408
+
do_action( 'tutor_order_enrolled', $order, $has_enrollment->ID );
409
+
}
410
+
}
411
+
} else {
412
+
if ( $order->order_status === $this->order_model::ORDER_COMPLETED ) {
413
+
// Insert enrollment.
414
+
add_filter( 'tutor_enroll_data', fn( $enroll_data) => array_merge( $enroll_data, array( 'post_status' => 'completed' ) ) );
415
+
416
+
$enrollment_id = tutor_utils()->do_enroll( $object_id, $order_id, $student_id );
417
+
if ( $enrollment_id ) {
418
+
if ( $this->is_bundle_order( $order, $object_id ) && $this->order_model->is_single_order( $order ) ) {
419
+
BundleModel::enroll_to_bundle_courses( $object_id, $student_id );
420
+
}
421
+
update_post_meta( $enrollment_id, '_tutor_enrolled_by_order_id', $order_id );
422
+
423
+
do_action( 'tutor_order_enrolled', $order, $enrollment_id );
424
+
} else {
425
+
// Log error message with student id and course id.
426
+
error_log( "Error updating enrollment for student {$student_id} and course {$object_id}" );
427
+
}
428
+
}
429
+
}
430
+
}
431
+
432
+
// Update earnings.
433
+
$earnings->prepare_order_earnings( $order_id );
434
+
$earnings->remove_before_store_earnings();
435
+
}
436
+
437
+
/**
438
+
* Update order data for the free checkout
439
+
*
440
+
* @since 3.4.0
441
+
*
442
+
* @param array $order_data Order data.
443
+
*
444
+
* @return array
445
+
*/
446
+
public function update_order_data( array $order_data ) {
447
+
if ( empty( $order_data['total_price'] ) && OrderModel::TYPE_SINGLE_ORDER === $order_data['order_type'] ) {
448
+
$order_data['order_status'] = OrderModel::ORDER_COMPLETED;
449
+
$order_data['payment_status'] = OrderModel::PAYMENT_PAID;
450
+
$order_data['payment_method'] = 'free';
451
+
}
452
+
return $order_data;
453
+
}
454
+
455
+
/**
456
+
* Enroll user to the course when free checkout
457
+
*
458
+
* @since 3.4.0
459
+
*
460
+
* @param array $order_data Order data.
461
+
*
462
+
* @return array
463
+
*/
464
+
public function handle_free_checkout( array $order_data ) {
465
+
if ( empty( $order_data['total_price'] ) && OrderModel::TYPE_SINGLE_ORDER === $order_data['order_type'] ) {
466
+
$order_id = $order_data['id'];
467
+
$user_id = $order_data['user_id'];
468
+
$items = $order_data['items'];
469
+
foreach ( $items as $item ) {
470
+
add_filter( 'tutor_enroll_data', fn( $enroll_data ) => array_merge( $enroll_data, array( 'post_status' => 'completed' ) ) );
471
+
472
+
$enrolled_id = tutor_utils()->do_enroll( $item['item_id'], $order_data['id'], $user_id );
473
+
if ( $enrolled_id && tutor_utils()->is_addon_enabled( 'course-bundle' ) && get_post_type( $item['item_id'] ) === CourseBundle::POST_TYPE ) {
474
+
BundleModel::enroll_to_bundle_courses( $item['item_id'], $user_id );
475
+
}
476
+
}
477
+
478
+
// Store coupon usage.
479
+
$this->coupon_ctrl->store_coupon_usage( $order_id );
480
+
}
481
+
return $order_data;
482
+
}
483
+
484
+
/**
485
+
* Redirect user to the course after free checkout when item is 1.
486
+
* If user checkout multiple items and keep the default behavior.
487
+
*
488
+
* @since 3.4.0
489
+
*
490
+
* @param string $url Default redirect url.
491
+
* @param string $status Order placement status.
492
+
* @param integer $order_id Order id.
493
+
*
494
+
* @return string
495
+
*/
496
+
public function redirect_to_the_course( string $url, string $status, int $order_id ): string {
497
+
$user_id = get_current_user_id();
498
+
if ( OrderModel::ORDER_PLACEMENT_SUCCESS === $status ) {
499
+
$order = $this->order_model->get_order_by_id( $order_id );
500
+
if ( $order && count( $order->items ) === 1 && empty( $order->total_price ) && OrderModel::TYPE_SINGLE_ORDER === $order->order_type ) {
501
+
502
+
// Firing hook to clear cart.
503
+
do_action( 'tutor_order_placement_success', $order_id );
504
+
505
+
// Clear the alert message.
506
+
delete_transient( CheckoutController::PAY_NOW_ALERT_MSG_TRANSIENT_KEY . $user_id );
507
+
delete_transient( CheckoutController::PAY_NOW_ERROR_TRANSIENT_KEY . $user_id );
508
+
$course_id = $order->items[0]->id;
509
+
$url = get_the_permalink( $course_id );
510
+
}
511
+
}
512
+
return $url;
513
+
}
514
+
515
+
/**
516
+
* Store billing address for an order when order is placed.
517
+
*
518
+
* @since 3.5.0
519
+
*
520
+
* @param array $order_data order data.
521
+
*
522
+
* @return void
523
+
*/
524
+
public function store_billing_address_for_order( array $order_data ) {
525
+
$order_id = $order_data['id'];
526
+
$user_id = $order_data['user_id'];
527
+
$billing_info = ( new BillingController( false ) )->get_billing_info( $user_id );
528
+
529
+
/**
530
+
* JSON_UNESCAPED_UNICODE is used to ensure that the billing info is stored in a readable format.
531
+
* This is important for languages that use non-ASCII characters like ñ, á, é, í, ó, ú, ü, etc.
532
+
*
533
+
* @since 3.7.1
534
+
*/
535
+
$meta_value = '{}';
536
+
if ( $billing_info ) {
537
+
$meta_value = wp_json_encode( $billing_info, JSON_UNESCAPED_UNICODE );
538
+
} else {
539
+
/**
540
+
* Store user data as billing info
541
+
* If user has no billing info during order like manual enrollment from CSV.
542
+
*/
543
+
$user_data = get_userdata( $user_id );
544
+
$meta_value = wp_json_encode(
545
+
array(
546
+
'billing_first_name' => $user_data->first_name,
547
+
'billing_last_name' => $user_data->last_name,
548
+
'billing_email' => $user_data->user_email,
549
+
),
550
+
JSON_UNESCAPED_UNICODE
551
+
);
552
+
}
553
+
554
+
OrderMetaModel::add_meta(
555
+
$order_id,
556
+
OrderModel::META_KEY_BILLING_ADDRESS,
557
+
$meta_value
558
+
);
559
+
}
560
+
}
561
+