booking
/
includes
/
page-setup-wizard
/
step-booking-form-template
/
class-wpbc-setup-wizard-booking-form-template-save-handler.php
class-wpbc-setup-wizard-booking-form-template-save-handler.php in Booking Calendar 11.9, at includes/page-setup-wizard/step-booking-form-template/class-wpbc-setup-wizard-booking-form-template-save-handler.php
| 1 | <?php |
| 2 | /** |
| 3 | * Progressive save handler for the Setup Wizard Booking Form step. |
| 4 | * |
| 5 | * @package Booking Calendar |
| 6 | */ |
| 7 | |
| 8 | if ( ! defined( 'ABSPATH' ) ) { |
| 9 | exit; |
| 10 | } |
| 11 | |
| 12 | /** |
| 13 | * Apply one allow-listed template to the Standard Booking Form. |
| 14 | * |
| 15 | * The owner-scoped Standard form is the idempotent Form Builder identity. |
| 16 | * Repeated requests therefore replace the same published row, while retained |
| 17 | * Guided Appointment additionally assigns the form to its saved Services. |
| 18 | * Other scheduled and full-day journeys intentionally keep the form independent |
| 19 | * of Appointment Services, because their routes do not contain a Services step. |
| 20 | */ |
| 21 | final class WPBC_Setup_Wizard_Booking_Form_Template_Save_Handler implements WPBC_Setup_Wizard_Step_Save_Handler { |
| 22 | |
| 23 | /** Canonical Form Builder identity updated by this Setup Wizard boundary. */ |
| 24 | const CANONICAL_FORM_SLUG = 'standard'; |
| 25 | |
| 26 | /** @var WPBC_Setup_Wizard_Booking_Form_Templates */ |
| 27 | private $templates; |
| 28 | |
| 29 | /** @var WPBC_Setup_Wizard_Booking_Form_Time_Options */ |
| 30 | private $time_options; |
| 31 | |
| 32 | /** |
| 33 | * Build the handler around the server-owned template registry. |
| 34 | * |
| 35 | * @param WPBC_Setup_Wizard_Booking_Form_Templates|null $templates Optional registry adapter for tests. |
| 36 | * @param WPBC_Setup_Wizard_Booking_Form_Time_Options|null $time_options Optional time projector for tests. |
| 37 | */ |
| 38 | public function __construct( $templates = null, $time_options = null ) { |
| 39 | $this->templates = $templates instanceof WPBC_Setup_Wizard_Booking_Form_Templates |
| 40 | ? $templates |
| 41 | : new WPBC_Setup_Wizard_Booking_Form_Templates(); |
| 42 | $this->time_options = $time_options instanceof WPBC_Setup_Wizard_Booking_Form_Time_Options |
| 43 | ? $time_options |
| 44 | : new WPBC_Setup_Wizard_Booking_Form_Time_Options(); |
| 45 | } |
| 46 | |
| 47 | /** |
| 48 | * Return the owned step identifier. |
| 49 | * |
| 50 | * @return string Stable step ID. |
| 51 | */ |
| 52 | public function get_step_id() { |
| 53 | return 'booking_form_template'; |
| 54 | } |
| 55 | |
| 56 | /** |
| 57 | * Describe whether the boundary creates or updates the Standard form. |
| 58 | * |
| 59 | * @param array<string,mixed> $checkpoint Current checkpoint. |
| 60 | * @param array<string,mixed> $step_context Authorized presentation context. |
| 61 | * |
| 62 | * @return string Translated action label. |
| 63 | */ |
| 64 | public function get_primary_action_label( array $checkpoint, array $step_context = array() ) { |
| 65 | unset( $checkpoint, $step_context ); |
| 66 | |
| 67 | $owner_user_id = $this->get_owner_user_id(); |
| 68 | $existing_form = function_exists( 'wpbc_form_config_load' ) |
| 69 | ? wpbc_form_config_load( self::CANONICAL_FORM_SLUG, $owner_user_id, 'published', false ) |
| 70 | : null; |
| 71 | |
| 72 | return is_array( $existing_form ) |
| 73 | ? __( 'Update booking form & continue', 'booking' ) |
| 74 | : __( 'Create booking form & continue', 'booking' ); |
| 75 | } |
| 76 | |
| 77 | /** |
| 78 | * Revalidate and persist the selected form and any required Service assignments. |
| 79 | * |
| 80 | * @param array<string,mixed> $validated_fields Validated current-step fields. |
| 81 | * @param array<string,mixed> $checkpoint Current checkpoint. |
| 82 | * @param array<string,mixed> $operation_context Stable operation metadata. |
| 83 | * |
| 84 | * @return array<string,mixed>|WP_Error Verified result or error. |
| 85 | */ |
| 86 | public function save( array $validated_fields, array $checkpoint, array $operation_context ) { |
| 87 | unset( $operation_context ); |
| 88 | |
| 89 | $customer_journey_id = $this->get_customer_journey_id( $checkpoint ); |
| 90 | $is_guided_journey = 'guided_appointment_flow' === $customer_journey_id; |
| 91 | if ( ! WPBC_Setup_Wizard_Customer_Journey_Policy::uses_booking_form_and_appearance_route( $customer_journey_id ) ) { |
| 92 | return new WP_Error( 'wpbc_setup_wizard_booking_form_journey_invalid', __( 'A Booking Form cannot be created for the selected customer journey.', 'booking' ) ); |
| 93 | } |
| 94 | $manage_capability = function_exists( 'wpbc_bfb_get_manage_cap' ) ? wpbc_bfb_get_manage_cap() : 'manage_options'; |
| 95 | if ( |
| 96 | ! current_user_can( $manage_capability ) |
| 97 | || ! function_exists( 'wpbc_form_config_load' ) |
| 98 | || ! function_exists( 'wpbc_form_config_save' ) |
| 99 | || ! class_exists( 'WPBC_BFB_Form_Storage' ) |
| 100 | ) { |
| 101 | return new WP_Error( 'wpbc_setup_wizard_booking_form_forbidden', __( 'You are not allowed to create or update this Booking Form.', 'booking' ) ); |
| 102 | } |
| 103 | if ( $is_guided_journey && ! function_exists( 'wpbc_appointment_services_get_data_provider' ) ) { |
| 104 | return new WP_Error( 'wpbc_setup_wizard_booking_form_storage_unsupported', __( 'The configured Appointment Services storage does not support Booking Form assignment from the Setup Wizard.', 'booking' ) ); |
| 105 | } |
| 106 | |
| 107 | $template_slug = isset( $validated_fields['booking_form_template'] ) |
| 108 | ? $this->templates->validate_template_slug( $validated_fields['booking_form_template'], true ) |
| 109 | : new WP_Error( 'wpbc_setup_wizard_booking_form_template_required', __( 'Choose a booking form template.', 'booking' ) ); |
| 110 | if ( is_wp_error( $template_slug ) ) { |
| 111 | return $template_slug; |
| 112 | } |
| 113 | $booking_form_usage = isset( $validated_fields['booking_form_usage'] ) |
| 114 | ? $this->templates->validate_booking_form_usage_for_journey( $validated_fields['booking_form_usage'], $customer_journey_id, true ) |
| 115 | : new WP_Error( 'wpbc_setup_wizard_booking_form_usage_required', __( 'Choose how customers will open the booking form.', 'booking' ) ); |
| 116 | if ( is_wp_error( $booking_form_usage ) ) { |
| 117 | return $booking_form_usage; |
| 118 | } |
| 119 | |
| 120 | $template_form_config = $this->templates->get_save_form_config( $template_slug ); |
| 121 | if ( is_wp_error( $template_form_config ) ) { |
| 122 | return $template_form_config; |
| 123 | } |
| 124 | $wizard_values = isset( $checkpoint['values'] ) && is_array( $checkpoint['values'] ) ? $checkpoint['values'] : array(); |
| 125 | $template_form_config = $this->time_options->apply_to_form_config( |
| 126 | $template_form_config, |
| 127 | $this->time_options->get_context_from_values( $wizard_values ) |
| 128 | ); |
| 129 | if ( is_wp_error( $template_form_config ) ) { |
| 130 | return $template_form_config; |
| 131 | } |
| 132 | |
| 133 | $owner_user_id = $this->get_owner_user_id(); |
| 134 | $existing_form = wpbc_form_config_load( self::CANONICAL_FORM_SLUG, $owner_user_id, 'published', false ); |
| 135 | $form_config = $this->build_standard_form_config( $template_form_config, $existing_form, $owner_user_id ); |
| 136 | $service_ids = array(); |
| 137 | $repository = null; |
| 138 | $service_before_images = array(); |
| 139 | if ( $is_guided_journey ) { |
| 140 | $service_ids = $this->get_service_ids( $checkpoint ); |
| 141 | if ( empty( $service_ids ) ) { |
| 142 | return new WP_Error( 'wpbc_setup_wizard_booking_form_services_missing', __( 'Save at least one Appointment Service before creating the Booking Form.', 'booking' ) ); |
| 143 | } |
| 144 | |
| 145 | $repository = wpbc_appointment_services_get_data_provider(); |
| 146 | if ( |
| 147 | ! is_object( $repository ) |
| 148 | || ! method_exists( $repository, 'get_deletion_before_image' ) |
| 149 | || ! method_exists( $repository, 'update_booking_form_id' ) |
| 150 | || ! method_exists( $repository, 'find' ) |
| 151 | ) { |
| 152 | return new WP_Error( 'wpbc_setup_wizard_booking_form_storage_unsupported', __( 'The configured Appointment Services storage does not support Booking Form assignment from the Setup Wizard.', 'booking' ) ); |
| 153 | } |
| 154 | |
| 155 | foreach ( $service_ids as $service_id ) { |
| 156 | $before_image = $repository->get_deletion_before_image( $service_id ); |
| 157 | if ( is_wp_error( $before_image ) || ! $this->is_current_owner_service( $before_image ) ) { |
| 158 | return new WP_Error( 'wpbc_setup_wizard_booking_form_service_stale', __( 'A saved Service changed or is no longer available. Return to Services and review it before continuing.', 'booking' ) ); |
| 159 | } |
| 160 | $service_before_images[ $service_id ] = $before_image; |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | $form_was_written = false; |
| 165 | if ( is_array( $existing_form ) && $this->form_config_matches( $form_config, $existing_form ) ) { |
| 166 | $booking_form_id = absint( $existing_form['id'] ); |
| 167 | } else { |
| 168 | $booking_form_id = wpbc_form_config_save( $form_config, array( 'sync_legacy' => false ) ); |
| 169 | $form_was_written = true; |
| 170 | if ( ! $booking_form_id ) { |
| 171 | return new WP_Error( 'wpbc_setup_wizard_booking_form_not_saved', __( 'The Booking Form could not be saved.', 'booking' ) ); |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | foreach ( $service_before_images as $service_id => $before_image ) { |
| 176 | if ( absint( $before_image['service']['booking_form_id'] ) === absint( $booking_form_id ) ) { |
| 177 | continue; |
| 178 | } |
| 179 | $saved_service = $repository->update_booking_form_id( $service_id, $booking_form_id ); |
| 180 | if ( is_wp_error( $saved_service ) ) { |
| 181 | $was_restored = $this->compensate( $repository, $existing_form, $form_config, $booking_form_id, $service_before_images, $form_was_written ); |
| 182 | $message = $was_restored |
| 183 | ? __( 'The Booking Form could not be assigned to every Service. Previous values were restored.', 'booking' ) |
| 184 | : __( 'The Booking Form could not be assigned to every Service, and previous values could not be restored completely. Review Booking Forms and Services before continuing.', 'booking' ); |
| 185 | return new WP_Error( 'wpbc_setup_wizard_booking_form_assignment_failed', $message ); |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | $verified_form = wpbc_form_config_load( self::CANONICAL_FORM_SLUG, $owner_user_id, 'published', false ); |
| 190 | if ( |
| 191 | ! is_array( $verified_form ) |
| 192 | || absint( $verified_form['id'] ) !== absint( $booking_form_id ) |
| 193 | || ! $this->form_config_matches( $form_config, $verified_form ) |
| 194 | ) { |
| 195 | $was_restored = $this->compensate( $repository, $existing_form, $form_config, $booking_form_id, $service_before_images, $form_was_written ); |
| 196 | $message = $was_restored |
| 197 | ? __( 'The Booking Form could not be verified. Previous values were restored.', 'booking' ) |
| 198 | : __( 'The Booking Form could not be verified, and previous values could not be restored completely. Review Booking Forms and Services before continuing.', 'booking' ); |
| 199 | return new WP_Error( 'wpbc_setup_wizard_booking_form_not_verified', $message ); |
| 200 | } |
| 201 | foreach ( $service_ids as $service_id ) { |
| 202 | $verified_service = $repository->find( $service_id ); |
| 203 | if ( is_wp_error( $verified_service ) || absint( $verified_service['booking_form_id'] ) !== absint( $booking_form_id ) ) { |
| 204 | $was_restored = $this->compensate( $repository, $existing_form, $form_config, $booking_form_id, $service_before_images, $form_was_written ); |
| 205 | $message = $was_restored |
| 206 | ? __( 'The Booking Form assignment could not be verified. Previous values were restored.', 'booking' ) |
| 207 | : __( 'The Booking Form assignment could not be verified, and previous values could not be restored completely. Review Booking Forms and Services before continuing.', 'booking' ); |
| 208 | return new WP_Error( 'wpbc_setup_wizard_booking_form_assignment_not_verified', $message ); |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | if ( $is_guided_journey ) { |
| 213 | $summary = is_array( $existing_form ) |
| 214 | ? __( 'Standard Booking Form updated and assigned to Services.', 'booking' ) |
| 215 | : __( 'Standard Booking Form created and assigned to Services.', 'booking' ); |
| 216 | } else { |
| 217 | $summary = is_array( $existing_form ) |
| 218 | ? __( 'Standard Booking Form updated.', 'booking' ) |
| 219 | : __( 'Standard Booking Form created.', 'booking' ); |
| 220 | } |
| 221 | |
| 222 | return array( |
| 223 | 'status' => 'saved', |
| 224 | 'summary' => $summary, |
| 225 | 'canonical_fingerprint' => hash( 'sha256', wp_json_encode( array( 'form' => $verified_form, 'source_template' => $template_slug, 'service_ids' => $service_ids, 'usage' => $booking_form_usage ) ) ), |
| 226 | 'created_ids' => is_array( $existing_form ) ? array() : array( 'booking_form:' . self::CANONICAL_FORM_SLUG . ':' . absint( $booking_form_id ) ), |
| 227 | 'updated_ids' => is_array( $existing_form ) ? array( 'booking_form:' . self::CANONICAL_FORM_SLUG . ':' . absint( $booking_form_id ) ) : array(), |
| 228 | 'warnings' => array(), |
| 229 | 'writes_canonical_settings' => true, |
| 230 | ); |
| 231 | } |
| 232 | |
| 233 | /** |
| 234 | * Retarget a registered template payload to the canonical Standard form. |
| 235 | * |
| 236 | * Applying a template in the Form Builder replaces the current form's field |
| 237 | * structure and settings without changing that form's identity or its |
| 238 | * administrator-facing details. The Setup Wizard follows the same contract: |
| 239 | * template content is copied into `standard`, while an existing Standard |
| 240 | * title, description, and picture remain owned by the Form Details editor. |
| 241 | * |
| 242 | * @param array<string,mixed> $template_form_config Server-owned template payload. |
| 243 | * @param array<string,mixed>|null $existing_form Existing Standard FormConfig, if any. |
| 244 | * @param int $owner_user_id Effective MultiUser owner ID. |
| 245 | * |
| 246 | * @return array<string,mixed> Canonical Standard FormConfig ready for persistence. |
| 247 | */ |
| 248 | private function build_standard_form_config( array $template_form_config, $existing_form, $owner_user_id ) { |
| 249 | $existing_form = is_array( $existing_form ) ? $existing_form : array(); |
| 250 | |
| 251 | $template_form_config['form_name'] = self::CANONICAL_FORM_SLUG; |
| 252 | $template_form_config['owner_user_id'] = absint( $owner_user_id ); |
| 253 | $template_form_config['scope'] = $owner_user_id ? 'user' : 'global'; |
| 254 | $template_form_config['status'] = 'published'; |
| 255 | $template_form_config['is_default'] = 1; |
| 256 | $template_form_config['booking_resource_id'] = null; |
| 257 | $template_form_config['title'] = array_key_exists( 'title', $existing_form ) |
| 258 | ? (string) $existing_form['title'] |
| 259 | : __( 'Standard', 'booking' ); |
| 260 | $template_form_config['description'] = array_key_exists( 'description', $existing_form ) |
| 261 | ? (string) $existing_form['description'] |
| 262 | : ''; |
| 263 | $template_form_config['picture_url'] = array_key_exists( 'picture_url', $existing_form ) |
| 264 | ? (string) $existing_form['picture_url'] |
| 265 | : ''; |
| 266 | |
| 267 | return $template_form_config; |
| 268 | } |
| 269 | |
| 270 | /** |
| 271 | * Restore form and Service before-images after a partial cross-domain write. |
| 272 | * |
| 273 | * @param WPBC_Appointment_Services_Repository|null $repository Canonical Services repository when assignments exist. |
| 274 | * @param array<string,mixed>|null $existing_form Original FormConfig, or null for a created form. |
| 275 | * @param array<string,mixed> $attempted_form Attempted FormConfig. |
| 276 | * @param int $booking_form_id Attempted canonical form ID. |
| 277 | * @param array<int,array<string,mixed>> $service_before_images Original Service records. |
| 278 | * @param bool $form_was_written Whether this request changed FormConfig storage. |
| 279 | * |
| 280 | * @return bool True when every Service and form compensation write succeeds. |
| 281 | */ |
| 282 | private function compensate( $repository, $existing_form, array $attempted_form, $booking_form_id, array $service_before_images, $form_was_written ) { |
| 283 | $was_restored = true; |
| 284 | if ( ! empty( $service_before_images ) && ! is_object( $repository ) ) { |
| 285 | return false; |
| 286 | } |
| 287 | foreach ( $service_before_images as $before_image ) { |
| 288 | $service_id = absint( $before_image['service']['service_id'] ); |
| 289 | $previous_booking_form_id = absint( $before_image['service']['booking_form_id'] ); |
| 290 | $restored_service = $repository->update_booking_form_id( $service_id, $previous_booking_form_id ); |
| 291 | if ( is_wp_error( $restored_service ) || absint( $restored_service['booking_form_id'] ) !== $previous_booking_form_id ) { |
| 292 | $was_restored = false; |
| 293 | } |
| 294 | } |
| 295 | if ( ! $form_was_written ) { |
| 296 | return $was_restored; |
| 297 | } |
| 298 | |
| 299 | if ( is_array( $existing_form ) ) { |
| 300 | $restored_form_id = wpbc_form_config_save( $existing_form, array( 'sync_legacy' => false ) ); |
| 301 | $restored_form = wpbc_form_config_load( $existing_form['form_name'], $existing_form['owner_user_id'], $existing_form['status'], false ); |
| 302 | if ( ! $restored_form_id || ! is_array( $restored_form ) || ! $this->form_config_matches( $existing_form, $restored_form ) ) { |
| 303 | $was_restored = false; |
| 304 | } |
| 305 | |
| 306 | return $was_restored; |
| 307 | } |
| 308 | |
| 309 | if ( ! WPBC_BFB_Form_Storage::delete_form_if_matches( |
| 310 | $booking_form_id, |
| 311 | $attempted_form['form_name'], |
| 312 | $attempted_form['status'], |
| 313 | $attempted_form['owner_user_id'] |
| 314 | ) ) { |
| 315 | $was_restored = false; |
| 316 | } elseif ( is_array( wpbc_form_config_load( $attempted_form['form_name'], $attempted_form['owner_user_id'], $attempted_form['status'], false ) ) ) { |
| 317 | $was_restored = false; |
| 318 | } |
| 319 | |
| 320 | return $was_restored; |
| 321 | } |
| 322 | |
| 323 | /** |
| 324 | * Compare the stable FormConfig fields owned by the selected template. |
| 325 | * |
| 326 | * Storage timestamps and the generated numeric ID are intentionally excluded. |
| 327 | * Form-style settings are global Appearance options and are stripped by the |
| 328 | * canonical Form Builder writer before the record is persisted. |
| 329 | * |
| 330 | * @param array<string,mixed> $expected Expected server-owned FormConfig. |
| 331 | * @param array<string,mixed> $actual Canonical FormConfig readback. |
| 332 | * |
| 333 | * @return bool True when every persisted template field matches. |
| 334 | */ |
| 335 | private function form_config_matches( array $expected, array $actual ) { |
| 336 | $expected_settings = isset( $expected['settings'] ) && is_array( $expected['settings'] ) ? $expected['settings'] : array(); |
| 337 | if ( function_exists( 'wpbc_bfb_settings__strip_form_style_options_from_form_settings' ) ) { |
| 338 | $expected_settings = wpbc_bfb_settings__strip_form_style_options_from_form_settings( $expected_settings ); |
| 339 | } |
| 340 | |
| 341 | $expected_values = array( |
| 342 | 'form_name' => isset( $expected['form_name'] ) ? (string) $expected['form_name'] : '', |
| 343 | 'owner_user_id' => isset( $expected['owner_user_id'] ) ? absint( $expected['owner_user_id'] ) : 0, |
| 344 | 'engine' => isset( $expected['engine'] ) ? (string) $expected['engine'] : 'bfb', |
| 345 | 'engine_version' => isset( $expected['engine_version'] ) ? (string) $expected['engine_version'] : '1.0', |
| 346 | 'title' => isset( $expected['title'] ) ? (string) $expected['title'] : '', |
| 347 | 'description' => isset( $expected['description'] ) ? (string) $expected['description'] : '', |
| 348 | 'scope' => isset( $expected['scope'] ) ? (string) $expected['scope'] : 'global', |
| 349 | 'status' => isset( $expected['status'] ) ? (string) $expected['status'] : 'published', |
| 350 | 'is_default' => ! empty( $expected['is_default'] ) ? 1 : 0, |
| 351 | 'booking_resource_id' => isset( $expected['booking_resource_id'] ) ? absint( $expected['booking_resource_id'] ) : null, |
| 352 | 'structure_json' => isset( $expected['structure_json'] ) ? (string) $expected['structure_json'] : '', |
| 353 | 'settings' => $expected_settings, |
| 354 | 'advanced_form' => isset( $expected['advanced_form'] ) ? (string) $expected['advanced_form'] : '', |
| 355 | 'content_form' => isset( $expected['content_form'] ) ? (string) $expected['content_form'] : '', |
| 356 | 'picture_url' => isset( $expected['picture_url'] ) ? (string) $expected['picture_url'] : '', |
| 357 | ); |
| 358 | $actual_values = array( |
| 359 | 'form_name' => isset( $actual['form_name'] ) ? (string) $actual['form_name'] : '', |
| 360 | 'owner_user_id' => isset( $actual['owner_user_id'] ) ? absint( $actual['owner_user_id'] ) : 0, |
| 361 | 'engine' => isset( $actual['engine'] ) ? (string) $actual['engine'] : 'bfb', |
| 362 | 'engine_version' => isset( $actual['engine_version'] ) ? (string) $actual['engine_version'] : '1.0', |
| 363 | 'title' => isset( $actual['title'] ) ? (string) $actual['title'] : '', |
| 364 | 'description' => isset( $actual['description'] ) ? (string) $actual['description'] : '', |
| 365 | 'scope' => isset( $actual['scope'] ) ? (string) $actual['scope'] : 'global', |
| 366 | 'status' => isset( $actual['status'] ) ? (string) $actual['status'] : 'published', |
| 367 | 'is_default' => ! empty( $actual['is_default'] ) ? 1 : 0, |
| 368 | 'booking_resource_id' => isset( $actual['booking_resource_id'] ) ? absint( $actual['booking_resource_id'] ) : null, |
| 369 | 'structure_json' => isset( $actual['structure_json'] ) ? (string) $actual['structure_json'] : '', |
| 370 | 'settings' => isset( $actual['settings'] ) && is_array( $actual['settings'] ) ? $actual['settings'] : array(), |
| 371 | 'advanced_form' => isset( $actual['advanced_form'] ) ? (string) $actual['advanced_form'] : '', |
| 372 | 'content_form' => isset( $actual['content_form'] ) ? (string) $actual['content_form'] : '', |
| 373 | 'picture_url' => isset( $actual['picture_url'] ) ? (string) $actual['picture_url'] : '', |
| 374 | ); |
| 375 | |
| 376 | return $expected_values === $actual_values; |
| 377 | } |
| 378 | |
| 379 | /** |
| 380 | * Resolve stable Service IDs from the saved draft and retained result map. |
| 381 | * |
| 382 | * @param array<string,mixed> $checkpoint Current checkpoint. |
| 383 | * |
| 384 | * @return int[] Owner-visible Service IDs. |
| 385 | */ |
| 386 | private function get_service_ids( array $checkpoint ) { |
| 387 | $result = isset( $checkpoint['step_results']['services'] ) && is_array( $checkpoint['step_results']['services'] ) ? $checkpoint['step_results']['services'] : array(); |
| 388 | $stored = array_merge( isset( $result['created_ids'] ) ? (array) $result['created_ids'] : array(), isset( $result['updated_ids'] ) ? (array) $result['updated_ids'] : array() ); |
| 389 | $mappings = array(); |
| 390 | foreach ( $stored as $mapping ) { |
| 391 | if ( is_scalar( $mapping ) && preg_match( '/^service:([a-z0-9_-]{1,64}):([0-9]+)$/', (string) $mapping, $matches ) ) { |
| 392 | $mappings[ $matches[1] ] = absint( $matches[2] ); |
| 393 | } |
| 394 | } |
| 395 | |
| 396 | $service_ids = array(); |
| 397 | $services = isset( $checkpoint['values']['services']['services'] ) && is_array( $checkpoint['values']['services']['services'] ) |
| 398 | ? $checkpoint['values']['services']['services'] |
| 399 | : array(); |
| 400 | foreach ( $services as $service ) { |
| 401 | $draft_id = isset( $service['draft_id'] ) ? sanitize_key( (string) $service['draft_id'] ) : ''; |
| 402 | $service_id = isset( $service['source_service_id'] ) ? absint( $service['source_service_id'] ) : 0; |
| 403 | if ( ! $service_id && isset( $mappings[ $draft_id ] ) ) { |
| 404 | $service_id = absint( $mappings[ $draft_id ] ); |
| 405 | } |
| 406 | if ( $service_id ) { |
| 407 | $service_ids[] = $service_id; |
| 408 | } |
| 409 | } |
| 410 | |
| 411 | return array_values( array_unique( $service_ids ) ); |
| 412 | } |
| 413 | |
| 414 | /** |
| 415 | * Resolve the current Form Builder owner without trusting request data. |
| 416 | * |
| 417 | * @return int Current MultiUser owner ID, or zero for global scope. |
| 418 | */ |
| 419 | private function get_owner_user_id() { |
| 420 | if ( function_exists( 'wpbc_appointment_services_get_owner_user_id' ) ) { |
| 421 | return absint( wpbc_appointment_services_get_owner_user_id() ); |
| 422 | } |
| 423 | |
| 424 | return class_exists( 'WPBC_FE_Custom_Form_Helper' ) && method_exists( 'WPBC_FE_Custom_Form_Helper', 'wpbc_mu__get_current__owner_user_id' ) |
| 425 | ? absint( WPBC_FE_Custom_Form_Helper::wpbc_mu__get_current__owner_user_id() ) |
| 426 | : 0; |
| 427 | } |
| 428 | |
| 429 | /** |
| 430 | * Confirm that one assignment target belongs to the FormConfig owner. |
| 431 | * |
| 432 | * @param array<string,mixed> $before_image Canonical Service before-image. |
| 433 | * |
| 434 | * @return bool True when the Service and Booking Form share one owner scope. |
| 435 | */ |
| 436 | private function is_current_owner_service( array $before_image ) { |
| 437 | $service = isset( $before_image['service'] ) && is_array( $before_image['service'] ) ? $before_image['service'] : array(); |
| 438 | |
| 439 | return isset( $service['owner_user_id'] ) && $this->get_owner_user_id() === absint( $service['owner_user_id'] ); |
| 440 | } |
| 441 | |
| 442 | /** |
| 443 | * Read the saved Customer Journey. |
| 444 | * |
| 445 | * @param array<string,mixed> $checkpoint Current checkpoint. |
| 446 | * |
| 447 | * @return string Journey ID. |
| 448 | */ |
| 449 | private function get_customer_journey_id( array $checkpoint ) { |
| 450 | return isset( $checkpoint['values']['customer_journey']['customer_journey'] ) |
| 451 | ? sanitize_key( (string) $checkpoint['values']['customer_journey']['customer_journey'] ) |
| 452 | : ''; |
| 453 | } |
| 454 | |
| 455 | /** |
| 456 | * Booking Form is not the terminal route step. |
| 457 | * |
| 458 | * @return bool False. |
| 459 | */ |
| 460 | public function is_terminal() { |
| 461 | return false; |
| 462 | } |
| 463 | } |
| 464 |