PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
booking / includes / page-setup-wizard / class-wpbc-setup-wizard-step-data.php

class-wpbc-setup-wizard-step-data.php in Booking Calendar 11.9, at includes/page-setup-wizard/class-wpbc-setup-wizard-step-data.php

960 lines 39.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Read-only presentation data for the Setup Wizard module.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Provide source-backed initial values and allow-listed field choices.
14 *
15 * This service reads current WordPress and Booking Calendar configuration but
16 * never writes it. Canonical-backed fields are refreshed whenever a step is
17 * opened; saved checkpoint values remain authoritative only for wizard-owned
18 * metadata that cannot be reconstructed safely from plugin state.
19 */
20 final class WPBC_Setup_Wizard_Step_Data {
21
22 /** @var WPBC_Setup_Wizard_Step_Module_Registry */
23 private $module_registry;
24
25 /**
26 * Build the read-only provider around the reusable module registry.
27 *
28 * @param WPBC_Setup_Wizard_Step_Module_Registry|null $module_registry Optional shared module registry.
29 */
30 public function __construct( $module_registry = null ) {
31 $this->module_registry = $module_registry instanceof WPBC_Setup_Wizard_Step_Module_Registry
32 ? $module_registry
33 : new WPBC_Setup_Wizard_Step_Module_Registry();
34 }
35
36 /**
37 * Return the presentation contract for one registered step.
38 *
39 * @param string $step_id Registered step identifier.
40 * @param array<string,mixed> $draft_values Normalized draft values keyed by step.
41 * @param array<string,mixed> $step_results Verified progressive-save results keyed by step.
42 *
43 * @return array<string,mixed> Authorized template context for the step.
44 */
45 public function get_step_context( $step_id, array $draft_values, array $step_results = array() ) {
46 $step_values = $this->get_step_values( $step_id, $draft_values );
47 $step_module = $this->module_registry->get_module( $step_id );
48
49 if ( null !== $step_module ) {
50 $consumer_context = array();
51 foreach ( $step_module->get_context_dependencies() as $dependency_step_id ) {
52 $consumer_context[ $dependency_step_id ] = $this->get_step_values( $dependency_step_id, $draft_values );
53 }
54 $consumer_context['_step_state'] = array(
55 'has_saved_values' => isset( $draft_values[ $step_id ] )
56 && is_array( $draft_values[ $step_id ] )
57 && ! empty( $draft_values[ $step_id ] ),
58 );
59 if ( ! empty( $step_results ) ) {
60 $consumer_context['_step_results'] = $step_results;
61 }
62
63 return $step_module->get_context( $step_values, $consumer_context );
64 }
65
66 switch ( $step_id ) {
67 case 'business_details':
68 return array(
69 'values' => $step_values,
70 'business_stages' => $this->get_business_stages(),
71 'industries' => $this->get_industry_groups(),
72 );
73
74 case 'booking_experience':
75 return array(
76 'values' => $step_values,
77 'experiences' => $this->get_booking_experiences(),
78 );
79
80 case 'customer_journey':
81 $selected_booking_mode_id = $this->get_selected_booking_mode_id( $draft_values );
82 $journey_groups = $this->get_customer_journey_groups( $selected_booking_mode_id );
83 $ordered_journeys = array();
84
85 foreach ( $journey_groups as $journey_group ) {
86 foreach ( $journey_group['journeys'] as $journey_id => $journey ) {
87 $ordered_journeys[ $journey_id ] = $journey;
88 }
89 }
90
91 return array(
92 'values' => $step_values,
93 'selected_booking_mode_id' => $selected_booking_mode_id,
94 'journey_groups' => $journey_groups,
95 'journeys' => $ordered_journeys,
96 );
97
98 default:
99 return array( 'values' => array() );
100 }
101 }
102
103 /**
104 * Resolve current presentation values for one step.
105 *
106 * Checkpoint values normally preserve prior wizard choices. A module may mark
107 * allow-listed fields as current-value fields, in which case the latest
108 * server-side initial value is overlaid after the checkpoint merge. This
109 * prevents Back, rail navigation, and the review page from presenting stale
110 * copies of canonical settings changed outside the wizard.
111 *
112 * @param string $step_id Registered step identifier.
113 * @param array<string,mixed> $draft_values Normalized draft values keyed by step.
114 *
115 * @return array<string,mixed> Values ready for the step template.
116 */
117 public function get_step_values( $step_id, array $draft_values ) {
118 $initial_values = $this->get_initial_values( $step_id );
119 $saved_values = isset( $draft_values[ $step_id ] ) && is_array( $draft_values[ $step_id ] )
120 ? $draft_values[ $step_id ]
121 : array();
122 $step_values = array_merge( $initial_values, array_intersect_key( $saved_values, $initial_values ) );
123 $current_fields = array_intersect(
124 $this->get_current_value_field_names( $step_id ),
125 array_keys( $initial_values )
126 );
127
128 if ( empty( $current_fields ) ) {
129 return $step_values;
130 }
131
132 return array_merge(
133 $step_values,
134 array_intersect_key( $initial_values, array_flip( $current_fields ) )
135 );
136 }
137
138 /**
139 * Return fields whose current server values override checkpoint snapshots.
140 *
141 * Booking Mode is the only core step backed directly by canonical plugin
142 * state. Reusable domain modules opt in through the companion current-values
143 * contract. Business Details and Customer Journey remain wizard metadata.
144 *
145 * @param string $step_id Registered step identifier.
146 *
147 * @return string[] Current-value field identifiers.
148 */
149 public function get_current_value_field_names( $step_id ) {
150 $step_module = $this->module_registry->get_module( $step_id );
151 if ( $step_module instanceof WPBC_Setup_Wizard_Current_Values_Module ) {
152 return array_values(
153 array_intersect(
154 $step_module->get_current_value_field_names(),
155 $step_module->get_field_names()
156 )
157 );
158 }
159
160 return 'booking_experience' === $step_id ? array( 'booking_experience' ) : array();
161 }
162
163 /**
164 * Return current canonical values or source-supported suggestions for a step.
165 *
166 * No value returned here is persisted unless the user explicitly continues
167 * or goes back from the corresponding Setup Wizard step.
168 *
169 * @param string $step_id Registered step identifier.
170 *
171 * @return array<string,mixed> Read-only initial values.
172 */
173 public function get_initial_values( $step_id ) {
174 $step_module = $this->module_registry->get_module( $step_id );
175 if ( null !== $step_module ) {
176 return $step_module->get_initial_values();
177 }
178
179 switch ( $step_id ) {
180 case 'business_details':
181 return array(
182 'business_name' => sanitize_text_field( (string) get_option( 'blogname', '' ) ),
183 'business_stage' => 'starting',
184 'industry' => 'other',
185 'booking_email' => sanitize_email( (string) get_option( 'admin_email', '' ) ),
186 'personalization_consent' => true,
187 );
188
189 case 'booking_experience':
190 $selected_mode_id = function_exists( 'wpbc_booking_modes_get_selected_mode_id' )
191 ? wpbc_booking_modes_get_selected_mode_id()
192 : 'classic';
193 $available_mode_ids = array_column( $this->get_booking_experiences(), 'id' );
194 $fallback_mode_id = in_array( 'classic', $available_mode_ids, true )
195 ? 'classic'
196 : ( ! empty( $available_mode_ids ) ? (string) reset( $available_mode_ids ) : 'classic' );
197
198 return array(
199 'booking_experience' => in_array( $selected_mode_id, $available_mode_ids, true ) ? $selected_mode_id : $fallback_mode_id,
200 );
201
202 case 'customer_journey':
203 return array(
204 'customer_journey' => '',
205 );
206
207 default:
208 return array();
209 }
210 }
211
212 /**
213 * Return field names accepted for one step.
214 *
215 * @param string $step_id Registered step identifier.
216 *
217 * @return string[] Ordered allow-list of draft field names.
218 */
219 public function get_field_names( $step_id ) {
220 $step_module = $this->module_registry->get_module( $step_id );
221 if ( null !== $step_module ) {
222 return $step_module->get_field_names();
223 }
224
225 $field_names = array(
226 'welcome' => array(),
227 'business_details' => array( 'business_name', 'business_stage', 'industry', 'booking_email', 'personalization_consent' ),
228 'booking_experience' => array( 'booking_experience' ),
229 'customer_journey' => array( 'customer_journey' ),
230 );
231
232 return isset( $field_names[ $step_id ] ) ? $field_names[ $step_id ] : array();
233 }
234
235 /**
236 * Return allowed scalar choices for one select, radio, or checkbox field.
237 *
238 * @param string $step_id Registered step identifier.
239 * @param string $field_id Stable field identifier.
240 *
241 * @return array<int,string|bool> Allowed values, or an empty array for free text.
242 */
243 public function get_allowed_values( $step_id, $field_id ) {
244 $available_customer_journey_ids = array();
245 foreach ( array_keys( self::get_customer_journeys() ) as $customer_journey_id ) {
246 if ( WPBC_Setup_Wizard_Customer_Journey_Policy::is_journey_available( $customer_journey_id ) ) {
247 $available_customer_journey_ids[] = $customer_journey_id;
248 }
249 }
250
251 $field_choices = array(
252 'business_details' => array(
253 'business_stage' => array_keys( $this->get_business_stages() ),
254 'industry' => array_keys( $this->get_industry_options() ),
255 'personalization_consent' => array( false, true ),
256 ),
257 'booking_experience' => array(
258 'booking_experience' => array_column( $this->get_booking_experiences(), 'id' ),
259 ),
260 'customer_journey' => array(
261 'customer_journey' => $available_customer_journey_ids,
262 ),
263 );
264
265 return isset( $field_choices[ $step_id ][ $field_id ] ) ? $field_choices[ $step_id ][ $field_id ] : array();
266 }
267
268 /**
269 * Return required fields for forward navigation from one step.
270 *
271 * @param string $step_id Registered step identifier.
272 *
273 * @return string[] Required field identifiers.
274 */
275 public function get_required_fields( $step_id ) {
276 $step_module = $this->module_registry->get_module( $step_id );
277 if ( null !== $step_module ) {
278 return $step_module->get_required_fields();
279 }
280
281 $required_fields = array(
282 'business_details' => array( 'business_name', 'business_stage', 'industry', 'booking_email' ),
283 'booking_experience' => array( 'booking_experience' ),
284 'customer_journey' => array( 'customer_journey' ),
285 );
286
287 return isset( $required_fields[ $step_id ] ) ? $required_fields[ $step_id ] : array();
288 }
289
290 /**
291 * Determine whether a step delegates its complete field contract to a module.
292 *
293 * @param string $step_id Registered step identifier.
294 *
295 * @return bool True when a reusable module owns the step.
296 */
297 public function is_module_step( $step_id ) {
298 return null !== $this->module_registry->get_module( $step_id );
299 }
300
301 /**
302 * Validate one field through its registered reusable module.
303 *
304 * @param string $step_id Registered step identifier.
305 * @param string $field_id Stable field identifier.
306 * @param mixed $raw_value Untrusted submitted or stored value.
307 * @param bool $is_required Whether an empty value is invalid.
308 *
309 * @return mixed|WP_Error Normalized field value or an error.
310 */
311 public function validate_module_field( $step_id, $field_id, $raw_value, $is_required ) {
312 $step_module = $this->module_registry->get_module( $step_id );
313
314 if ( null === $step_module ) {
315 return new WP_Error( 'wpbc_setup_wizard_step_module_missing', __( 'The requested setup editor is not registered.', 'booking' ) );
316 }
317
318 return $step_module->validate_field( $field_id, $raw_value, $is_required );
319 }
320
321 /**
322 * Validate relationships across a reusable module's normalized fields.
323 *
324 * Modules without the companion values-validator contract keep the existing
325 * field-only behavior. The consumer remains domain-neutral and does not know
326 * which field relationships a module owns.
327 *
328 * @param string $step_id Registered step identifier.
329 * @param array<string,mixed> $validated_values Values after individual field validation.
330 * @param array<string,mixed> $draft_values Normalized draft values keyed by step.
331 *
332 * @return array<string,mixed>|WP_Error Validated values or a module-owned error.
333 */
334 public function validate_module_values( $step_id, array $validated_values, array $draft_values = array() ) {
335 $step_module = $this->module_registry->get_module( $step_id );
336
337 if ( null === $step_module ) {
338 return new WP_Error( 'wpbc_setup_wizard_step_module_missing', __( 'The requested setup editor is not registered.', 'booking' ) );
339 }
340
341 if ( $step_module instanceof WPBC_Setup_Wizard_Step_Values_Validator ) {
342 $validated_values = $step_module->validate_values( $validated_values );
343 if ( is_wp_error( $validated_values ) ) {
344 return $validated_values;
345 }
346 }
347
348 if ( ! $step_module instanceof WPBC_Setup_Wizard_Contextual_Values_Validator ) {
349 return $validated_values;
350 }
351
352 $consumer_context = array();
353 foreach ( $step_module->get_context_dependencies() as $dependency_step_id ) {
354 $consumer_context[ $dependency_step_id ] = $this->get_step_values( $dependency_step_id, $draft_values );
355 }
356
357 return $step_module->validate_values_with_context( $validated_values, $consumer_context );
358 }
359
360 /**
361 * Return business maturity options retained from the released wizard.
362 *
363 * @return array<string,string> Labels keyed by stable draft value.
364 */
365 public function get_business_stages() {
366 return array(
367 'starting' => __( "I'm just starting my business", 'booking' ),
368 'in_business' => __( "I'm already in business", 'booking' ),
369 'setup_for_client' => __( "I'm setting up a plugin for a client", 'booking' ),
370 );
371 }
372
373 /**
374 * Return grouped industry choices retained from the released wizard.
375 *
376 * @return array<int,array{label:string,options:array<string,string>}> Industry groups.
377 */
378 public function get_industry_groups() {
379 return array(
380 array(
381 'label' => __( 'Hospitality & Property', 'booking' ),
382 'options' => $this->translate_options(
383 array(
384 'property_rentals' => 'Property Rentals',
385 'hotels' => 'Hotels',
386 'resorts' => 'Resorts',
387 'rentals' => 'Vacation Rentals',
388 'coworking_spaces' => 'Coworking Spaces',
389 'other_hospitality' => 'Other',
390 )
391 ),
392 ),
393 array(
394 'label' => __( 'Transportation & Activities', 'booking' ),
395 'options' => $this->translate_options(
396 array(
397 'car_rentals' => 'Car Rentals',
398 'shuttles' => 'Shuttle Services',
399 'limousines' => 'Limousines',
400 'tour_operators' => 'Tour Operators',
401 'adventure_parks' => 'Adventure Parks',
402 'other_transportation' => 'Other',
403 )
404 ),
405 ),
406 array(
407 'label' => __( 'Events & Entertainment', 'booking' ),
408 'options' => $this->translate_options(
409 array(
410 'event_venues' => 'Event Venues',
411 'meeting_rooms' => 'Meeting Rooms',
412 'escape_rooms' => 'Escape Rooms',
413 'other_events' => 'Other',
414 )
415 ),
416 ),
417 array(
418 'label' => __( 'Health & Wellness', 'booking' ),
419 'options' => $this->translate_options(
420 array(
421 'spas' => 'Spas',
422 'salons' => 'Salons',
423 'clinics' => 'Clinics',
424 'massage_therapy' => 'Massage Therapy',
425 'other_health' => 'Other',
426 )
427 ),
428 ),
429 array(
430 'label' => __( 'Healthcare', 'booking' ),
431 'options' => $this->translate_options(
432 array(
433 'doctors' => 'Doctors',
434 'dentists' => 'Dentists',
435 'therapists' => 'Therapists',
436 'chiropractors' => 'Chiropractors',
437 'acupuncturists' => 'Acupuncturists',
438 'other_healthcare' => 'Other',
439 )
440 ),
441 ),
442 array(
443 'label' => __( 'Professional Services', 'booking' ),
444 'options' => $this->translate_options(
445 array(
446 'consultants' => 'Consultants',
447 'lawyers' => 'Lawyers',
448 'accountants' => 'Accountants',
449 'financial_advisors' => 'Financial Advisors',
450 'other_professional' => 'Other',
451 )
452 ),
453 ),
454 array(
455 'label' => __( 'Retail & E-Commerce', 'booking' ),
456 'options' => $this->translate_options(
457 array(
458 'product_demos' => 'Product Demonstrations',
459 'fitting_appointments' => 'Fitting Appointments',
460 'equipment_rentals' => 'Equipment Rentals',
461 'other_retail' => 'Other',
462 )
463 ),
464 ),
465 array(
466 'label' => __( 'Education & Training', 'booking' ),
467 'options' => $this->translate_options(
468 array(
469 'tutors' => 'Tutors',
470 'classes' => 'Classes',
471 'schools' => 'Schools',
472 'workshops' => 'Workshops',
473 'other_education' => 'Other',
474 )
475 ),
476 ),
477 array(
478 'label' => __( 'Food & Restaurants', 'booking' ),
479 'options' => $this->translate_options(
480 array(
481 'table_reservations' => 'Table Reservations',
482 'catering' => 'Catering',
483 'banquet_halls' => 'Banquet Halls',
484 'other_food' => 'Other',
485 )
486 ),
487 ),
488 array(
489 'label' => __( 'Creative & Technical Services', 'booking' ),
490 'options' => $this->translate_options(
491 array(
492 'photographers' => 'Photographers',
493 'videographers' => 'Videographers',
494 'web_developers' => 'Web Developers',
495 'it_support' => 'IT Support',
496 'art_studios' => 'Art Studios',
497 'music_lessons' => 'Music Lessons',
498 'other_creative' => 'Other',
499 )
500 ),
501 ),
502 array(
503 'label' => __( 'Legal Services', 'booking' ),
504 'options' => $this->translate_options(
505 array(
506 'attorneys' => 'Attorneys',
507 'legal_consultants' => 'Legal Consultants',
508 'other_legal' => 'Other',
509 )
510 ),
511 ),
512 array(
513 'label' => __( 'Childcare & Home Services', 'booking' ),
514 'options' => $this->translate_options(
515 array(
516 'daycares' => 'Daycares',
517 'babysitting' => 'Babysitting',
518 'nanny_services' => 'Nanny Services',
519 'cleaners' => 'Cleaners',
520 'repairs' => 'Repair Services',
521 'handyman' => 'Handyman',
522 'other_childcare' => 'Other',
523 )
524 ),
525 ),
526 array(
527 'label' => __( 'Sports & Fitness', 'booking' ),
528 'options' => $this->translate_options(
529 array(
530 'gyms' => 'Gyms',
531 'fitness_classes' => 'Fitness Classes',
532 'yoga_studios' => 'Yoga Studios',
533 'golf_lessons' => 'Golf Lessons',
534 'sports_equipment_rentals' => 'Sports Equipment Rentals',
535 'other_sports' => 'Other',
536 )
537 ),
538 ),
539 array(
540 'label' => __( 'Pet Services', 'booking' ),
541 'options' => $this->translate_options(
542 array(
543 'veterinarians' => 'Veterinarians',
544 'groomers' => 'Pet Groomers',
545 'pet_sitting' => 'Pet Sitting',
546 'other_pet' => 'Other',
547 )
548 ),
549 ),
550 array(
551 'label' => __( 'Personal Services', 'booking' ),
552 'options' => $this->translate_options(
553 array(
554 'coaching' => 'Coaching',
555 'counseling' => 'Counseling',
556 'spiritual_services' => 'Spiritual Services',
557 'interior_design' => 'Interior Design',
558 'other_personal' => 'Other',
559 )
560 ),
561 ),
562 array(
563 'label' => __( 'Other', 'booking' ),
564 'options' => $this->translate_options( array( 'other' => 'Other' ) ),
565 ),
566 );
567 }
568
569 /**
570 * Flatten grouped industries for validation.
571 *
572 * @return array<string,string> Industry labels keyed by stable identifier.
573 */
574 private function get_industry_options() {
575 $industry_options = array();
576
577 foreach ( $this->get_industry_groups() as $industry_group ) {
578 $industry_options = array_merge( $industry_options, $industry_group['options'] );
579 }
580
581 return $industry_options;
582 }
583
584 /**
585 * Translate a compact source-defined option map.
586 *
587 * @param array<string,string> $options English labels keyed by stable identifiers.
588 *
589 * @return array<string,string> Translated labels keyed by stable identifiers.
590 */
591 private function translate_options( array $options ) {
592 $translated_labels = array(
593 'Property Rentals' => __( 'Property Rentals', 'booking' ),
594 'Hotels' => __( 'Hotels', 'booking' ),
595 'Resorts' => __( 'Resorts', 'booking' ),
596 'Vacation Rentals' => __( 'Vacation Rentals', 'booking' ),
597 'Coworking Spaces' => __( 'Coworking Spaces', 'booking' ),
598 'Other' => __( 'Other', 'booking' ),
599 'Car Rentals' => __( 'Car Rentals', 'booking' ),
600 'Shuttle Services' => __( 'Shuttle Services', 'booking' ),
601 'Limousines' => __( 'Limousines', 'booking' ),
602 'Tour Operators' => __( 'Tour Operators', 'booking' ),
603 'Adventure Parks' => __( 'Adventure Parks', 'booking' ),
604 'Event Venues' => __( 'Event Venues', 'booking' ),
605 'Meeting Rooms' => __( 'Meeting Rooms', 'booking' ),
606 'Escape Rooms' => __( 'Escape Rooms', 'booking' ),
607 'Spas' => __( 'Spas', 'booking' ),
608 'Salons' => __( 'Salons', 'booking' ),
609 'Clinics' => __( 'Clinics', 'booking' ),
610 'Massage Therapy' => __( 'Massage Therapy', 'booking' ),
611 'Doctors' => __( 'Doctors', 'booking' ),
612 'Dentists' => __( 'Dentists', 'booking' ),
613 'Therapists' => __( 'Therapists', 'booking' ),
614 'Chiropractors' => __( 'Chiropractors', 'booking' ),
615 'Acupuncturists' => __( 'Acupuncturists', 'booking' ),
616 'Consultants' => __( 'Consultants', 'booking' ),
617 'Lawyers' => __( 'Lawyers', 'booking' ),
618 'Accountants' => __( 'Accountants', 'booking' ),
619 'Financial Advisors' => __( 'Financial Advisors', 'booking' ),
620 'Product Demonstrations' => __( 'Product Demonstrations', 'booking' ),
621 'Fitting Appointments' => __( 'Fitting Appointments', 'booking' ),
622 'Equipment Rentals' => __( 'Equipment Rentals', 'booking' ),
623 'Tutors' => __( 'Tutors', 'booking' ),
624 'Classes' => __( 'Classes', 'booking' ),
625 'Schools' => __( 'Schools', 'booking' ),
626 'Workshops' => __( 'Workshops', 'booking' ),
627 'Table Reservations' => __( 'Table Reservations', 'booking' ),
628 'Catering' => __( 'Catering', 'booking' ),
629 'Banquet Halls' => __( 'Banquet Halls', 'booking' ),
630 'Photographers' => __( 'Photographers', 'booking' ),
631 'Videographers' => __( 'Videographers', 'booking' ),
632 'Web Developers' => __( 'Web Developers', 'booking' ),
633 'IT Support' => __( 'IT Support', 'booking' ),
634 'Art Studios' => __( 'Art Studios', 'booking' ),
635 'Music Lessons' => __( 'Music Lessons', 'booking' ),
636 'Attorneys' => __( 'Attorneys', 'booking' ),
637 'Legal Consultants' => __( 'Legal Consultants', 'booking' ),
638 'Daycares' => __( 'Daycares', 'booking' ),
639 'Babysitting' => __( 'Babysitting', 'booking' ),
640 'Nanny Services' => __( 'Nanny Services', 'booking' ),
641 'Cleaners' => __( 'Cleaners', 'booking' ),
642 'Repair Services' => __( 'Repair Services', 'booking' ),
643 'Handyman' => __( 'Handyman', 'booking' ),
644 'Gyms' => __( 'Gyms', 'booking' ),
645 'Fitness Classes' => __( 'Fitness Classes', 'booking' ),
646 'Yoga Studios' => __( 'Yoga Studios', 'booking' ),
647 'Golf Lessons' => __( 'Golf Lessons', 'booking' ),
648 'Sports Equipment Rentals' => __( 'Sports Equipment Rentals', 'booking' ),
649 'Veterinarians' => __( 'Veterinarians', 'booking' ),
650 'Pet Groomers' => __( 'Pet Groomers', 'booking' ),
651 'Pet Sitting' => __( 'Pet Sitting', 'booking' ),
652 'Coaching' => __( 'Coaching', 'booking' ),
653 'Counseling' => __( 'Counseling', 'booking' ),
654 'Spiritual Services' => __( 'Spiritual Services', 'booking' ),
655 'Interior Design' => __( 'Interior Design', 'booking' ),
656 );
657
658 foreach ( $options as $option_id => $option_label ) {
659 $options[ $option_id ] = isset( $translated_labels[ $option_label ] ) ? $translated_labels[ $option_label ] : $option_label;
660 }
661
662 return $options;
663 }
664
665 /**
666 * Return the bundled Booking Mode terminology choices allowed in this context.
667 *
668 * Stable identifiers come from the existing Booking Modes setup integration.
669 * The remaining records are presentation-only metadata for the approved
670 * administration-navigation mockup. They never authorize or apply a mode.
671 *
672 * @return array<int,array<string,mixed>> Card presentation records.
673 */
674 private function get_booking_experiences() {
675 $allowed_mode_ids = array( 'appointment', 'rental', 'classic' );
676 if ( function_exists( 'wpbc_booking_modes_get_setup_mode_choices' ) ) {
677 $allowed_mode_ids = array_keys( wpbc_booking_modes_get_setup_mode_choices() );
678 } elseif ( function_exists( 'wpbc_booking_modes_get_allowed_mode_ids' ) ) {
679 $allowed_mode_ids = wpbc_booking_modes_get_allowed_mode_ids();
680 }
681
682 $experience_definitions = array(
683 'appointment' => array(
684 'id' => 'appointment',
685 'title' => __( 'Appointments & services', 'booking' ),
686 'toolbar_label' => __( 'Appointments', 'booking' ),
687 'admin_navigation' => array(
688 array( 'label' => __( 'Appointments', 'booking' ), 'icon' => 'wpbc_icn_event_available' ),
689 array( 'label' => __( 'Services', 'booking' ), 'icon' => 'wpbc_icn_local_offer' ),
690 array( 'label' => __( 'Providers', 'booking' ), 'icon' => 'wpbc_icn_groups' ),
691 array( 'label' => __( 'Working hours', 'booking' ), 'icon' => 'wpbc_icn_access_time' ),
692 ),
693 'dashboard_uses' => __( 'Appointments, Services, Providers and Working hours.', 'booking' ),
694 'best_for' => __( 'Consultations, salons, classes and scheduled services.', 'booking' ),
695 ),
696 'rental' => array(
697 'id' => 'rental',
698 'title' => __( 'Properties & rentals', 'booking' ),
699 'toolbar_label' => __( 'Rentals', 'booking' ),
700 'admin_navigation' => array(
701 array( 'label' => __( 'Bookings', 'booking' ), 'icon' => 'wpbc_icn_event' ),
702 array( 'label' => __( 'Properties', 'booking' ), 'icon' => 'wpbc_icn_home' ),
703 array( 'label' => __( 'Availability', 'booking' ), 'icon' => 'wpbc_icn_date_range' ),
704 array( 'label' => __( 'Guests', 'booking' ), 'icon' => 'wpbc_icn_groups' ),
705 ),
706 'dashboard_uses' => __( 'Bookings, Properties, Availability and Guests.', 'booking' ),
707 'best_for' => __( 'Rooms, equipment and holiday rentals.', 'booking' ),
708 ),
709 'classic' => array(
710 'id' => 'classic',
711 'title' => __( 'Classic Booking Calendar', 'booking' ),
712 'toolbar_label' => __( 'Classic', 'booking' ),
713 'admin_navigation' => array(
714 array( 'label' => __( 'Bookings', 'booking' ), 'icon' => 'wpbc_icn_event' ),
715 array( 'label' => __( 'Booking resources', 'booking' ), 'icon' => 'wpbc_icn_dashboard' ),
716 array( 'label' => __( 'Availability', 'booking' ), 'icon' => 'wpbc_icn_date_range' ),
717 array( 'label' => __( 'Booking forms', 'booking' ), 'icon' => 'wpbc_icn_description' ),
718 ),
719 'dashboard_uses' => __( 'Bookings, Booking resources, Availability and Booking forms.', 'booking' ),
720 'best_for' => __( 'Traditional resource-first booking.', 'booking' ),
721 ),
722 );
723
724 $experiences = array();
725 foreach ( array( 'appointment', 'rental', 'classic' ) as $mode_id ) {
726 if ( in_array( $mode_id, $allowed_mode_ids, true ) ) {
727 $experience = $experience_definitions[ $mode_id ];
728 $mode = function_exists( 'wpbc_booking_modes_get_mode' ) ? wpbc_booking_modes_get_mode( $mode_id ) : null;
729
730 if ( is_array( $mode ) && ! empty( $mode['label'] ) && is_scalar( $mode['label'] ) ) {
731 $experience['toolbar_label'] = sanitize_text_field( (string) $mode['label'] );
732 }
733
734 /* translators: %s: Booking Calendar administration mode. */
735 $experience['toolbar_title'] = sprintf( __( 'Mode: %s', 'booking' ), $experience['toolbar_label'] );
736 $experiences[] = $experience;
737 }
738 }
739
740 return $experiences;
741 }
742
743 /**
744 * Resolve the Page 4 Booking Mode proposal used to organize Page 5.
745 *
746 * The current owner-scoped Booking Mode always wins over an older checkpoint
747 * snapshot. This method never writes the canonical `booking_admin_mode` user
748 * option.
749 *
750 * @param array<string,mixed> $draft_values Normalized draft values by step.
751 *
752 * @return string One allowed bundled Booking Mode identifier.
753 */
754 private function get_selected_booking_mode_id( array $draft_values ) {
755 $experience_ids = array_column( $this->get_booking_experiences(), 'id' );
756 $experience_values = $this->get_step_values( 'booking_experience', $draft_values );
757 $selected_mode_id = isset( $experience_values['booking_experience'] )
758 ? sanitize_key( (string) $experience_values['booking_experience'] )
759 : '';
760
761 if ( in_array( $selected_mode_id, $experience_ids, true ) ) {
762 return $selected_mode_id;
763 }
764
765 return ! empty( $experience_ids ) ? (string) reset( $experience_ids ) : 'classic';
766 }
767
768 /**
769 * Build the Page 5 recommended and additional groups for one Booking Mode.
770 *
771 * All registered journeys remain visible. Branch-compatible journeys are
772 * ordered first, while the remaining choices are presented as visually muted
773 * additional options. Edition-owned availability is attached by the master
774 * registry and independently enforced by the field allow-list, so presentation
775 * state never becomes an authorization boundary.
776 *
777 * @param string $mode_id Validated Page 4 Booking Mode identifier.
778 *
779 * @return array<int,array<string,mixed>> Ordered presentation groups.
780 */
781 public function get_customer_journey_groups( $mode_id ) {
782 $mode_id = sanitize_key( (string) $mode_id );
783 $journeys = self::get_customer_journeys();
784 $recommended_order = array(
785 'appointment' => array( 'guided_appointment_flow', 'start_end_time', 'start_time_duration', 'fixed_time_slots' ),
786 'rental' => array( 'single_full_day', 'multiple_independent_days', 'flexible_date_range', 'fixed_length_date_range', 'check_in_out_changeover' ),
787 'classic' => array( 'single_full_day', 'multiple_independent_days', 'flexible_date_range', 'fixed_length_date_range', 'check_in_out_changeover', 'fixed_time_slots', 'start_end_time', 'start_time_duration', 'repeated_time_multiple_dates', 'first_start_last_end' ),
788 );
789
790 if ( ! isset( $recommended_order[ $mode_id ] ) ) {
791 $mode_id = 'classic';
792 }
793
794 $recommended_journeys = array();
795 foreach ( $recommended_order[ $mode_id ] as $journey_id ) {
796 if ( isset( $journeys[ $journey_id ] ) && in_array( $mode_id, $journeys[ $journey_id ]['branches'], true ) ) {
797 $recommended_journeys[ $journey_id ] = $journeys[ $journey_id ];
798 }
799 }
800
801 foreach ( $journeys as $journey_id => $journey ) {
802 if ( in_array( $mode_id, $journey['branches'], true ) && ! isset( $recommended_journeys[ $journey_id ] ) ) {
803 $recommended_journeys[ $journey_id ] = $journey;
804 }
805 }
806
807 $additional_journeys = array_diff_key( $journeys, $recommended_journeys );
808 $mode_titles = array(
809 'appointment' => __( 'Appointments & services', 'booking' ),
810 'rental' => __( 'Properties & rentals', 'booking' ),
811 'classic' => __( 'Classic Booking Calendar', 'booking' ),
812 );
813
814 $groups = array(
815 array(
816 'id' => 'recommended',
817 /* translators: %s: Selected Booking Mode title. */
818 'label' => sprintf( __( 'Recommended for %s', 'booking' ), $mode_titles[ $mode_id ] ),
819 'description' => __( 'These customer journeys best match the admin terminology selected in the previous step.', 'booking' ),
820 'is_additional' => false,
821 'journeys' => $recommended_journeys,
822 ),
823 );
824
825 if ( ! empty( $additional_journeys ) ) {
826 $groups[] = array(
827 'id' => 'additional',
828 'label' => __( 'Additional customer journeys', 'booking' ),
829 'description' => __( 'These alternatives remain available when your customer flow differs from the selected admin terminology.', 'booking' ),
830 'is_additional' => true,
831 'journeys' => $additional_journeys,
832 );
833 }
834
835 return $groups;
836 }
837
838 /**
839 * Return the complete Phase 3 customer-journey registry.
840 *
841 * The registry intentionally returns every unique source-backed journey.
842 * The `branches` metadata lets Page 5 recommend and order journeys for the
843 * Page 4 Booking Mode without removing alternatives. Stable identifiers are
844 * the draft contract; labels, descriptions, previews, and branch metadata
845 * are presentation information.
846 *
847 * @return array<string,array<string,mixed>> Journey records keyed by stable identifier.
848 */
849 public static function get_customer_journeys() {
850 $journeys = array(
851 'guided_appointment_flow' => array(
852 'id' => 'guided_appointment_flow',
853 'title' => __( 'Guided appointment flow', 'booking' ),
854 'description' => __( 'Customers choose a Service, Provider and available start time step by step.', 'booking' ),
855 'journey' => array( __( 'Service', 'booking' ), __( 'Provider', 'booking' ), __( 'Date', 'booking' ), __( 'Time', 'booking' ), __( 'Details', 'booking' ) ),
856 'preview_types' => array( 'service', 'providers', 'calendar', 'times', 'details' ),
857 'best_for' => __( 'Businesses with several Services or Providers.', 'booking' ),
858 'branches' => array( 'appointment' ),
859 ),
860 'start_end_time' => array(
861 'id' => 'start_end_time',
862 'title' => __( 'Start & end time', 'booking' ),
863 'description' => __( 'Customers choose both ends of a flexible appointment time range.', 'booking' ),
864 'journey' => array( __( 'Date', 'booking' ), __( 'Start time', 'booking' ), __( 'End time', 'booking' ), __( 'Details', 'booking' ) ),
865 'preview_types' => array( 'calendar', 'times', 'times', 'details' ),
866 'best_for' => __( 'Flexible appointments, equipment use and variable-duration work.', 'booking' ),
867 'branches' => array( 'appointment', 'classic' ),
868 ),
869 'start_time_duration' => array(
870 'id' => 'start_time_duration',
871 'title' => __( 'Start time + duration', 'booking' ),
872 'description' => __( 'Customers choose a start time and one of the available durations.', 'booking' ),
873 'journey' => array( __( 'Date', 'booking' ), __( 'Start time', 'booking' ), __( 'Duration', 'booking' ), __( 'Details', 'booking' ) ),
874 'preview_types' => array( 'calendar', 'times', 'times', 'details' ),
875 'best_for' => __( 'Appointments offered in standard duration choices.', 'booking' ),
876 'branches' => array( 'appointment', 'classic' ),
877 ),
878 'fixed_time_slots' => array(
879 'id' => 'fixed_time_slots',
880 'title' => __( 'Fixed time slots', 'booking' ),
881 'description' => __( 'Customers select one predefined available time slot.', 'booking' ),
882 'journey' => array( __( 'Date', 'booking' ), __( 'Available slot', 'booking' ), __( 'Details', 'booking' ) ),
883 'preview_types' => array( 'calendar', 'times', 'details' ),
884 'best_for' => __( 'Classes, consultations and fixed timetables.', 'booking' ),
885 'branches' => array( 'appointment', 'classic' ),
886 ),
887 'single_full_day' => array(
888 'id' => 'single_full_day',
889 'title' => __( 'Single full day', 'booking' ),
890 'description' => __( 'Customers reserve one complete date without choosing a time.', 'booking' ),
891 'journey' => array( __( 'Date', 'booking' ), __( 'Details', 'booking' ) ),
892 'preview_types' => array( 'calendar', 'details' ),
893 'best_for' => __( 'One-day rentals, events and daily reservations.', 'booking' ),
894 'branches' => array( 'rental', 'classic' ),
895 ),
896 'multiple_independent_days' => array(
897 'id' => 'multiple_independent_days',
898 'title' => __( 'Multiple independent full days', 'booking' ),
899 'description' => __( 'Customers select several separate full dates in one booking.', 'booking' ),
900 'journey' => array( __( 'Dates', 'booking' ), __( 'Details', 'booking' ) ),
901 'preview_types' => array( 'calendar-multiple', 'details' ),
902 'best_for' => __( 'Bookings that cover several non-consecutive days.', 'booking' ),
903 'branches' => array( 'rental', 'classic' ),
904 ),
905 'flexible_date_range' => array(
906 'id' => 'flexible_date_range',
907 'title' => __( 'Flexible date range', 'booking' ),
908 'description' => __( 'Customers choose the first and last date of a variable-length booking.', 'booking' ),
909 'journey' => array( __( 'Start date', 'booking' ), __( 'End date', 'booking' ), __( 'Details', 'booking' ) ),
910 'preview_types' => array( 'range-flexible-start', 'range-flexible-end', 'details' ),
911 'best_for' => __( 'Stays and rentals whose length varies by booking.', 'booking' ),
912 'branches' => array( 'rental', 'classic' ),
913 ),
914 'fixed_length_date_range' => array(
915 'id' => 'fixed_length_date_range',
916 'title' => __( 'Fixed-length date range', 'booking' ),
917 'description' => __( 'Customers choose a start date and the configured range is selected automatically.', 'booking' ),
918 'journey' => array( __( 'Start date', 'booking' ), __( 'Fixed stay', 'booking' ), __( 'Details', 'booking' ) ),
919 'preview_types' => array( 'range-fixed-start', 'range-fixed-selected', 'details' ),
920 'best_for' => __( 'Weekend, multi-day and weekly packages.', 'booking' ),
921 'branches' => array( 'rental', 'classic' ),
922 ),
923 'check_in_out_changeover' => array(
924 'id' => 'check_in_out_changeover',
925 'title' => __( 'Check-in & check-out changeover', 'booking' ),
926 'description' => __( 'Customers choose arrival and departure dates with changeover boundaries.', 'booking' ),
927 'journey' => array( __( 'Check-in', 'booking' ), __( 'Check-out', 'booking' ), __( 'Guest details', 'booking' ) ),
928 'preview_types' => array( 'range-check-in', 'range-check-out', 'guest-details' ),
929 'best_for' => __( 'Accommodation and rentals with arrival and departure times.', 'booking' ),
930 'branches' => array( 'rental', 'classic' ),
931 ),
932 'repeated_time_multiple_dates' => array(
933 'id' => 'repeated_time_multiple_dates',
934 'title' => __( 'Repeat one time on multiple dates', 'booking' ),
935 'description' => __( 'Customers select several dates and use the same time on every selected date.', 'booking' ),
936 'journey' => array( __( 'Dates', 'booking' ), __( 'Time', 'booking' ), __( 'Details', 'booking' ) ),
937 'preview_types' => array( 'calendar-multiple', 'times', 'details' ),
938 'best_for' => __( 'Recurring sessions submitted together for the same daily time.', 'booking' ),
939 'branches' => array( 'classic' ),
940 ),
941 'first_start_last_end' => array(
942 'id' => 'first_start_last_end',
943 'title' => __( 'First-date start & last-date end', 'booking' ),
944 'description' => __( 'Customers choose a start on the first date and an end on the last date.', 'booking' ),
945 'journey' => array( __( 'Start date & time', 'booking' ), __( 'End date & time', 'booking' ), __( 'Details', 'booking' ) ),
946 'preview_types' => array( 'date-time-start', 'date-time-end', 'details' ),
947 'best_for' => __( 'Multi-day bookings with different arrival and departure times.', 'booking' ),
948 'branches' => array( 'classic' ),
949 ),
950 );
951
952 foreach ( $journeys as $journey_id => $journey ) {
953 $journeys[ $journey_id ]['edition_requirement'] = WPBC_Setup_Wizard_Customer_Journey_Policy::get_journey_edition_requirement( $journey_id );
954 }
955
956 return $journeys;
957 }
958
959 }
960