Diff: STRATO-apps/wordpress_03/app/wp-content/plugins/tutor/ecommerce/HooksHandler.php

Keine Baseline-Datei – Diff nur gegen leer.
Zur Liste
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 +