PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.56
Timetics – Appointment Booking Calendar & Scheduling v1.0.56
1.0.61 1.0.60 1.0.59 1.0.58 1.0.57 1.0.56 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.2 1.0.20 1.0.21 1.0.22 1.0.23 1.0.24 All 62 releases
timetics / core / integrations / woocommerce / hooks.php

hooks.php in Timetics – Appointment Booking Calendar & Scheduling 1.0.56, at core/integrations/woocommerce/hooks.php

776 lines 24.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WooCommerce Hooks Class
4 *
5 * @package Timetics
6 */
7 namespace Timetics\Core\Integrations\Woocommerce;
8
9 defined( 'ABSPATH' ) || exit;
10
11 use Timetics\Core\Appointments\Appointment;
12 use Timetics\Core\Bookings\Booking;
13 use Timetics\Core\Customers\Customer;
14 use Timetics\Utils\Singleton;
15 use Timetics\Core\Emails\New_Event_Email;
16 use Timetics\Core\Emails\New_Event_Customer_Email;
17 use Timetics\Core\Emails\Cancel_Event_Email;
18 use Timetics\Core\Emails\Cancel_Event_Customer_Email;
19 use Timetics\Core\Integrations\Woocommerce\Status_Mapper;
20
21 /**
22 * Class Hooks
23 */
24 class Hooks {
25 use Singleton;
26
27 /**
28 * Initialize
29 *
30 * @return void
31 */
32 public function init() {
33 Cart_Api::instance();
34
35 add_action( 'woocommerce_init', [$this, 'load_woocommerce_required_files'] );
36
37 add_action( 'woocommerce_thankyou', [$this, 'update_booking_payment_status'] );
38
39 // Sync booking status when WooCommerce order status changes
40 add_action( 'woocommerce_order_status_changed', [$this, 'sync_booking_status_from_order'], 10, 3 );
41
42 add_filter( 'timetics_get_settings', [$this, 'add_woocommerce_currency_support'] );
43
44 add_filter( 'woocommerce_product_query', [$this, 'modify_woocommerce_shop'] );
45
46 add_action( 'woocommerce_coupon_options_usage_restriction', [$this, 'ensure_meeting_products_exist'], 5, 2 );
47 add_action( 'woocommerce_coupon_options_usage_restriction', [$this, 'add_meeting_coupon_restriction_option'], 10, 2 );
48
49 add_action( 'timetics_meeting_after_insert', [$this, 'clear_meeting_products_cache'], 99, 2 );
50 add_action( 'before_delete_post', [$this, 'maybe_clear_cache_on_meeting_delete'], 10, 2 );
51
52 add_action( 'woocommerce_coupon_options_save', [$this, 'save_coupon_option'], 10, 2 );
53
54 add_filter( 'woocommerce_coupon_get_products', [$this, 'assign_meeting_coupon_products'], 10, 2 );
55
56 add_filter( 'timetics_settings', [$this, 'add_wc_checkout_url'] );
57
58 add_filter( 'woocommerce_checkout_fields', [$this, 'hide_checkout_fields'] );
59
60 add_filter( 'woocommerce_checkout_posted_data', [$this, 'modify_order_data'] );
61
62 add_action( 'timetics_after_booking_create', [$this, 'add_product_to_cart'], 10, 4 );
63
64 add_filter( 'timetics_currency', [ $this, 'support_woocommerce_currency' ] );
65
66 add_action( 'admin_init', [ $this, 'create_term_timetics_meeting' ] );
67
68 add_action( 'wp_head', [ $this, 'hide_regular_price_checkout_css' ] );
69
70 // Display meeting details on checkout page
71 add_action( 'woocommerce_review_order_before_payment', [ $this, 'display_meeting_details_on_checkout' ] );
72
73 // Sync order status when booking status changes (bidirectional sync)
74 add_action( 'transition_post_status', [ $this, 'sync_order_status_from_booking' ], 10, 3 );
75 }
76
77 /**
78 * Load all required files and functions
79 *
80 * @return void
81 */
82 public function load_woocommerce_required_files() {
83 if ( ! WC()->is_rest_api_request() ) {
84 return;
85 }
86
87 WC()->frontend_includes();
88
89 if ( null === WC()->cart && function_exists( 'wc_load_cart' ) ) {
90 wc_load_cart();
91 }
92 }
93
94 /**
95 * Add product data on woocommerce product data
96 *
97 * @return void
98 */
99 public function add_product_data_store( $stores ) {
100 $stores['product'] = 'Timetics\Core\Integrations\Woocommerce\Product_Data_Store';
101
102 return $stores;
103 }
104
105 /**
106 * Update booking payment status
107 *
108 * @param integer $order_id
109 *
110 * @return void
111 */
112 public function update_booking_payment_status( $order_id ) {
113
114 $order = wc_get_order( $order_id );
115
116 if ( ! $order->is_paid() ) {
117 // Mark booking as failed if payment was not completed
118 $session_data = WC()->session->get( 'timetics_data' );
119 if ( $session_data ) {
120 $booking = new Booking( $session_data['booking_id'] );
121 Status_Mapper::set_order_id_for_booking( $session_data['booking_id'], $order_id );
122 }
123 WC()->session->set( 'timetics_data', null );
124 return;
125 }
126
127 $session_data = WC()->session->get( 'timetics_data' );
128
129 if ( ! $session_data ) {
130 return;
131 }
132
133 $booking = new Booking( $session_data['booking_id'] );
134
135 // Get the default booking status (e.g., 'approved')
136 $default_booking_status = timetics_get_option( 'default_booking_status', 'approved' );
137
138 $booking->update( [
139 'post_status' => $default_booking_status,
140 'payment_status' => 'completed',
141 'payment_method' => 'woocommerce',
142 ] );
143
144 // Store the order ID in booking meta and booking id in woocommerce order meta for status syncing
145 Status_Mapper::set_order_id_for_booking( $session_data['booking_id'], $order_id );
146 Status_Mapper::set_booking_id_for_order( $order_id, $session_data['booking_id'] );
147
148 $booking->create_event();
149 $is_email_to_customer = timetics_get_option( 'booking_created_customer');
150 $is_email_to_host = timetics_get_option( 'booking_created_host');
151 if( $is_email_to_host ){
152 $new_event_email = new New_Event_Email( $booking );
153 $new_event_email->send();
154 }
155
156 if( $is_email_to_customer ){
157 $new_event_customer_email = new New_Event_Customer_Email( $booking );
158 $new_event_customer_email->send();
159 }
160
161 WC()->session->set( 'timetics_data', null );
162
163 }
164
165 /**
166 * Support woocommerce currency
167 *
168 * @param array $settings
169 *
170 * @return array
171 */
172 public function add_woocommerce_currency_support( $settings ) {
173
174 if ( ! function_exists( 'WC' ) ) {
175 return $settings;
176 }
177
178 $wc_integration = timetics_get_option( 'wc_integration' );
179
180 if ( ! $wc_integration ) {
181 return $settings;
182 }
183
184 $settings['currency'] = get_woocommerce_currency();
185
186 return $settings;
187 }
188
189 /**
190 * Hide timetics meeting from woocommerce shop page
191 *
192 * @param Object $query
193 *
194 * @return Object
195 */
196 public function modify_woocommerce_shop( $query ) {
197 $category_slug = 'timetics-meeting'; // Replace 'category-slug' with the desired category slug
198
199 // Get product category ID
200 $category = get_term_by( 'slug', $category_slug, 'product_cat' );
201
202 if ( $category ) {
203 $category_id = $category->term_id;
204
205 // Exclude products assigned to the category
206 $query->set( 'tax_query', array(
207 array(
208 'taxonomy' => 'product_cat',
209 'field' => 'term_id',
210 'terms' => $category_id,
211 'operator' => 'NOT IN',
212 ),
213 ) );
214 }
215
216 return $query;
217 }
218
219 /**
220 * Set coupon products
221 *
222 * @param integer $coupon_id
223 * @param Object $coupon
224 *
225 * @return void
226 */
227 public function save_coupon_option( $coupon_id, $coupon ) {
228 // Verify nonce for security
229 if ( ! isset( $_POST['timetics_coupon_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['timetics_coupon_nonce'] ) ), 'timetics_coupon_action' ) ) {
230 return;
231 }
232
233 $meeting_ids = ! empty( $_POST['timetics_meeting_ids'] ) ? array_map( 'intval', wp_unslash( $_POST['timetics_meeting_ids'] ) ) : [];
234
235 $coupon_products = $coupon->get_product_ids();
236 $coupon_products = array_merge( $coupon_products, $meeting_ids );
237 $coupon->update_meta_data( 'timetics_meeting_ids', $meeting_ids );
238
239 $coupon->set_product_ids( $coupon_products );
240 $coupon->save();
241 }
242
243 /**
244 * Add woocommerce coupon restriction option
245 *
246 * @param int $coupon_id
247 * @param Object $coupon
248 *
249 * @return void
250 */
251 public function add_meeting_coupon_restriction_option( $coupon_id, $coupon ) {
252 include_once TIMETICS_PLUGIN_DIR . '/templates/woocommerce/coupon-restriction-option.php';
253 }
254
255 /**
256 * Ensure all meeting products exist in WooCommerce
257 *
258 * @param int $coupon_id
259 * @param Object $coupon
260 *
261 * @return void
262 */
263 public function ensure_meeting_products_exist( $coupon_id, $coupon ) {
264 $cache_key = 'timetics_meeting_products_synced';
265 if ( get_transient( $cache_key ) ) {
266 return;
267 }
268
269 if ( ! term_exists( 'timetics-meeting', 'product_cat' ) ) {
270 timetics_add_woocommerce_product_cat();
271 }
272
273 $args = [
274 'post_type' => 'timetics-appointment',
275 'post_status' => 'publish',
276 'posts_per_page' => -1,
277 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering meetings by meta fields
278 'meta_query' => [
279 [
280 'key' => 'wc_product_id',
281 'compare' => 'NOT EXISTS',
282 ],
283 ],
284 ];
285
286 $meetings = get_posts( $args );
287
288 if ( empty( $meetings ) ) {
289 set_transient( $cache_key, true, DAY_IN_SECONDS );
290 return;
291 }
292
293 $product_creator = new Product();
294
295 foreach ( $meetings as $meeting ) {
296 $appointment = new Appointment( $meeting->ID );
297
298 if ( ! $appointment->get_id() ) {
299 continue;
300 }
301
302 if ( empty( $appointment->get_name() ) ) {
303 continue;
304 }
305
306 $product_id = $appointment->get_wc_product_id();
307 if ( ! $product_id ) {
308 $price = $appointment->get_price();
309 if ( is_array( $price ) ) {
310 $price = '';
311 }
312
313 $product_id = $product_creator->create( $appointment );
314 }
315 }
316
317 set_transient( $cache_key, true, DAY_IN_SECONDS );
318 }
319
320 /**
321 * Clear meeting products cache when new meeting is created
322 *
323 * @param mixed $appointment Appointment object or ID
324 * @param mixed $data Meeting data array
325 *
326 * @return void
327 */
328 public function clear_meeting_products_cache( $appointment = null, $data = null ) {
329 delete_transient( 'timetics_meeting_products_synced' );
330 }
331
332 /**
333 * Clear meeting products cache when a meeting is deleted
334 *
335 * @param int $post_id
336 * @param Object $post
337 *
338 * @return void
339 */
340 public function maybe_clear_cache_on_meeting_delete( $post_id, $post ) {
341 if ( 'timetics-appointment' === $post->post_type ) {
342 delete_transient( 'timetics_meeting_products_synced' );
343 }
344 }
345
346 /**
347 * Add checkout url on settings
348 *
349 * @param array $settings
350 *
351 * @return array
352 */
353 public function add_wc_checkout_url( $settings ) {
354 if ( ! timetics_is_woocommerce_active() ) {
355 return $settings;
356 }
357
358 $settings['wc_checkout_url'] = wc_get_checkout_url();
359
360 return $settings;
361 }
362
363 /**
364 * Hide all fields from checkout page
365 *
366 * @param array $fields
367 *
368 * @return array
369 */
370 public function hide_checkout_fields( $fields ) {
371 $session_data = WC()->session->get( 'timetics_data' );
372
373 if ( ! $session_data ) {
374 return $fields;
375 }
376
377 $fields['billing'] = array();
378
379 // Hide all shipping fields
380 $fields['shipping'] = array();
381
382 $fields['order'] = array();
383
384 return $fields;
385 }
386
387 /**
388 * Modify order posted data
389 *
390 * @param array $data
391 *
392 * @return array
393 */
394 public function modify_order_data( $data ) {
395 $session_data = WC()->session->get( 'timetics_data' );
396
397 if ( ! $session_data ) {
398 return $data;
399 }
400
401 $booking = new Booking( $session_data['booking_id'] );
402 $customer = new Customer( $booking->get_customer_id() );
403
404 $first_name = $customer->get_first_name();
405 $last_name = $customer->get_last_name();
406 $email = $customer->get_email();
407 $phone = $customer->get_phone();
408
409 if ( $first_name ) {
410 $data['billing_first_name'] = $first_name;
411 }
412
413 if ( $last_name ) {
414 $data['billing_last_name'] = $last_name;
415 }
416
417 if ( $email ) {
418 $data['billing_email'] = $email;
419 }
420
421 if ( $phone ) {
422 $data['billing_phone'] = $phone;
423 }
424
425 return $data;
426 }
427
428 /**
429 * Add to cart timetics appointment
430 *
431 * @param integer $booking_id
432 * @param integer $customer_id
433 * @param integer $meeting_id
434 * @param array $data
435 *
436 * @return void
437 */
438 public function add_product_to_cart( $booking_id, $customer_id, $meeting_id, $data ) {
439 if ( 'woocommerce' !== $data['payment_method'] ) {
440 return;
441 }
442
443 $booking = new Booking( $booking_id );
444 $meeting_id = $booking->get_appointment();
445 if( !empty($data['seats']) && is_array( $data['seats'])) {
446 $quantity = count( $data['seats'] );
447 }else {
448 $quantity = 1;
449 }
450 $price = $booking->get_total();
451
452 // Get meeting details for checkout display
453 $appointment = new Appointment( $meeting_id );
454 $start_date = $booking->get_start_date();
455 $start_time = $booking->get_start_time();
456 $end_time = $booking->get_end_time();
457 $timezone = $booking->get_timezone();
458 $location = $booking->get_location();
459 $location_type = $booking->get_location_type();
460 $duration = $appointment->get_duration();
461 $meeting_timezone = $appointment->get_timezone();
462
463 // Set session for timetics data for woocommerce.
464 WC()->session->set( 'timetics_data', [
465 'booking_id' => $booking_id,
466 'meeting_id' => $meeting_id,
467 'price' => $price,
468 'start_date' => $start_date,
469 'start_time' => $start_time,
470 'end_time' => $end_time,
471 'timezone' => $timezone,
472 'meeting_timezone' => $meeting_timezone,
473 'location' => $location,
474 'location_type' => $location_type,
475 'duration' => $duration,
476 ] );
477
478 // Remove all items from cart.
479 WC()->cart->empty_cart();
480
481 // Preparing for add to cart.
482 $meeting = new Appointment( $meeting_id );
483 $product_id = $meeting->get_wc_product_id();
484
485
486 if ( ! $product_id ) {
487 $product = new Product();
488 $product_id = $product->create( $meeting );
489 }
490
491 $wc_product = wc_get_product( $product_id );
492 if ( ! $wc_product->get_price() ) {
493 update_post_meta( $product_id, '_regular_price', wc_format_decimal( $price ) );
494 update_post_meta( $product_id, '_price', wc_format_decimal( $price ) );
495 }
496
497 if ( ! $product_id ) {
498 return;
499 }
500
501 WC()->cart->add_to_cart( $product_id, $quantity );
502 }
503
504 /**
505 * Suport woocmmerce currency
506 *
507 * @param string $currency
508 *
509 * @return string
510 */
511 public function support_woocommerce_currency( $currency ) {
512 if ( ! timetics_is_woocommerce_active() ) {
513 return $currency;
514 }
515
516 return get_woocommerce_currency();
517 }
518
519 /**
520 * Create a category if woocommerce loaded
521 *
522 * @return void
523 */
524 public function create_term_timetics_meeting() {
525 if ( term_exists( 'timetics-meeting', 'product_cat' ) ) {
526 return;
527 }
528
529 // Create new woocommerce product category for timetics meeting
530 timetics_add_woocommerce_product_cat();
531 }
532
533 public function hide_regular_price_checkout_css() {
534 $session = WC()->session;
535 if ( (is_checkout() || is_cart()) && $session ) {
536 $timetics_data = $session->get('timetics_data');
537 if( isset($timetics_data) ) {
538 ?>
539 <style type="text/css">
540 .wc-block-components-order-summary .wc-block-components-order-summary-item__individual-prices {
541 display: none;
542 }
543 .wc-block-cart-item__prices,
544 .woocommerce-cart-form__cart-item.cart_item .product-price {
545 display: none;
546 }
547 .wc-block-cart-item__quantity,
548 .woocommerce-cart-form__cart-item.cart_item .product-quantity {
549 display: none;
550 }
551 </style>
552 <?php
553 }
554 }
555 }
556
557 /**
558 * Format and prepare meeting details
559 *
560 * @param array $session_data
561 *
562 * @return array|false
563 */
564 private function prepare_meeting_details( $session_data ) {
565 if ( ! $session_data || ! is_array( $session_data ) ) {
566 return false;
567 }
568
569 $start_date = isset( $session_data['start_date'] ) ? $session_data['start_date'] : '';
570 $start_time = isset( $session_data['start_time'] ) ? $session_data['start_time'] : '';
571 $customer_timezone = isset( $session_data['timezone'] ) ? $session_data['timezone'] : '';
572 $meeting_timezone = isset( $session_data['meeting_timezone'] ) ? $session_data['meeting_timezone'] : '';
573 $duration = isset( $session_data['duration'] ) ? $session_data['duration'] : '';
574 $location_type = isset( $session_data['location_type'] ) ? $session_data['location_type'] : '';
575
576 if ( ! $start_date || ! $start_time ) {
577 return false;
578 }
579
580 // Get WordPress date and time formats
581 $date_format = get_option( 'date_format', 'F j, Y' );
582 $time_format = get_option( 'time_format', 'g:i a' );
583
584 // Convert timezone if both customer and meeting timezones are available
585 $datetime_to_display = null;
586
587 if ( $customer_timezone && $meeting_timezone && timetics_is_valid_timezone( $customer_timezone ) && timetics_is_valid_timezone( $meeting_timezone ) ) {
588 $datetime_to_display = timetics_convert_timezone( $start_date . ' ' . $start_time, $customer_timezone, $meeting_timezone );
589 $display_timezone = $meeting_timezone;
590 } else {
591 $datetime_to_display = new \DateTime( $start_date . ' ' . $start_time );
592 $display_timezone = $customer_timezone ?: 'UTC';
593 }
594
595 $formatted_date = $datetime_to_display->format( $date_format );
596 $formatted_time = $datetime_to_display->format( $time_format );
597
598 // Build location type label
599 $location_label = '';
600 if ( $location_type ) {
601 $location_label = ucfirst( str_replace( '-', ' ', $location_type ) );
602 }
603
604 return [
605 'formatted_date' => $formatted_date,
606 'formatted_time' => $formatted_time,
607 'duration' => $duration,
608 'location_label' => $location_label,
609 'timezone' => $customer_timezone,
610 'display_timezone' => $display_timezone,
611 ];
612 }
613
614 /**
615 * Display meeting details on checkout page
616 *
617 * @return void
618 */
619 public function display_meeting_details_on_checkout() {
620 if ( ! is_checkout() ) {
621 return;
622 }
623
624 $session_data = WC()->session->get( 'timetics_data' );
625 $details = $this->prepare_meeting_details( $session_data );
626
627 if ( ! $details ) {
628 return;
629 }
630
631 $timetics_formatted_date = $details['formatted_date'];
632 $timetics_formatted_time = $details['formatted_time'];
633 $timetics_duration = $details['duration'];
634 $timetics_location_label = $details['location_label'];
635 $timetics_timezone = $details['timezone'];
636 $timetics_display_timezone = $details['display_timezone'];
637
638 include TIMETICS_PLUGIN_DIR . '/templates/woocommerce/meeting-details-checkout.php';
639 }
640
641 /**
642 * Sync booking status when WooCommerce order status changes
643 *
644 * @param int $order_id The WooCommerce order ID
645 * @param string $old_status The previous order status (without 'wc-' prefix)
646 * @param string $new_status The new order status (without 'wc-' prefix)
647 *
648 * @return void
649 */
650 public function sync_booking_status_from_order( $order_id, $old_status, $new_status ) {
651 // Get the booking ID from order meta
652 $booking_id = Status_Mapper::get_booking_id_from_order( $order_id );
653
654 if ( ! $booking_id ) {
655 return;
656 }
657
658 // Check if this pair is already syncing (prevents reverse sync infinite loop)
659 if ( Status_Mapper::is_syncing_pair( $booking_id, $order_id ) ) {
660 return;
661 }
662
663 // Check if sync is needed
664 if ( ! Status_Mapper::should_sync_status( $old_status, $new_status ) ) {
665 return;
666 }
667
668 // Mark this pair as syncing to prevent reverse sync
669 Status_Mapper::begin_sync_pair( $booking_id, $order_id );
670
671 try {
672 $booking = new Booking( $booking_id );
673
674 if ( ! $booking->is_booking() ) {
675 return;
676 }
677
678 // Map WooCommerce status to Timetics booking status
679 $booking_status = Status_Mapper::order_to_booking_status( $new_status );
680
681 // Get current booking status
682 $current_status = $booking->get_status();
683
684 // Only update if status has actually changed
685 if ( $current_status === $booking_status ) {
686 return;
687 }
688
689 // Update booking status
690 // This will trigger transition_post_status, but our pair lock prevents reverse sync
691 $booking->update( [ 'post_status' => $booking_status ] );
692
693 // Send cancellation emails if status changed to 'cancel'
694 if ( 'cancel' === $booking_status ) {
695 $booking->delete_event();
696
697 $is_email_to_customer = timetics_get_option( 'booking_canceled_customer' );
698 $is_email_to_host = timetics_get_option( 'booking_canceled_host' );
699
700 if ( $is_email_to_host ) {
701 $cancel_event_email = new Cancel_Event_Email( $booking );
702 $cancel_event_email->send();
703 }
704
705 if ( $is_email_to_customer ) {
706 $customer_cancel_event_email = new Cancel_Event_Customer_Email( $booking );
707 $customer_cancel_event_email->send();
708 }
709 }
710
711 } finally {
712 // Always clear the sync pair flag
713 Status_Mapper::end_sync_pair( $booking_id, $order_id );
714 }
715 }
716
717 /**
718 * Sync WooCommerce order status when booking status changes
719 *
720 * @param string $new_status The new booking status
721 * @param string $old_status The previous booking status
722 * @param WP_Post $post The booking post object
723 *
724 * @return void
725 */
726 public function sync_order_status_from_booking( $new_status, $old_status, $post ) {
727 // Only process timetics-booking posts
728 if ( 'timetics-booking' !== $post->post_type ) {
729 return;
730 }
731
732 $booking_id = $post->ID;
733
734 $order_id = Status_Mapper::get_order_id_from_booking( $booking_id );
735
736 if ( ! $order_id ) {
737 return;
738 }
739
740 if ( Status_Mapper::is_syncing_pair( $booking_id, $order_id ) ) {
741 return;
742 }
743
744 if ( ! Status_Mapper::should_sync_status( $old_status, $new_status ) ) {
745 return;
746 }
747
748 Status_Mapper::begin_sync_pair( $booking_id, $order_id );
749
750 try {
751 $order = wc_get_order( $order_id );
752
753 if ( ! $order ) {
754 return;
755 }
756
757 // Map Timetics booking status to WooCommerce order status
758 $wc_status = Status_Mapper::booking_to_order_status( $new_status );
759
760 // Get current order status (without 'wc-' prefix)
761 $current_wc_status = str_replace( 'wc-', '', $order->get_status() );
762
763 if ( $current_wc_status === $wc_status ) {
764 return;
765 }
766
767 $order->set_status( $wc_status );
768 $order->save();
769
770 } finally {
771 // Always clear the sync pair flag
772 Status_Mapper::end_sync_pair( $booking_id, $order_id );
773 }
774 }
775 }
776