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 / step-services / class-wpbc-setup-wizard-services-save-handler.php

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

562 lines 24.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Progressive save handler for Setup Wizard Appointment Services.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Create, update, or move Services to Draft for a service-based journey.
14 *
15 * Draft identifiers are mapped to canonical Service IDs in step-result metadata.
16 * That mapping makes Back, double-click retries, and later Booking Form assignment
17 * deterministic. A separate explicit lifecycle field stages Draft transitions;
18 * omission alone is non-mutating and this handler never permanently deletes a
19 * Service.
20 */
21 final class WPBC_Setup_Wizard_Services_Save_Handler implements WPBC_Setup_Wizard_Step_Save_Handler {
22
23 /** @var WPBC_Setup_Wizard_Services */
24 private $services;
25
26 /** @var WPBC_Setup_Wizard_Services_Module */
27 private $services_module;
28
29 /**
30 * Build the handler around the Services domain validator.
31 *
32 * @param WPBC_Setup_Wizard_Services|null $services Optional domain service for tests.
33 */
34 public function __construct( $services = null ) {
35 $this->services = $services instanceof WPBC_Setup_Wizard_Services
36 ? $services
37 : new WPBC_Setup_Wizard_Services();
38 $this->services_module = new WPBC_Setup_Wizard_Services_Module( $this->services );
39 }
40
41 /**
42 * Return the owned step identifier.
43 *
44 * @return string Stable step ID.
45 */
46 public function get_step_id() {
47 return 'services';
48 }
49
50 /**
51 * Describe whether the next boundary creates or updates canonical records.
52 *
53 * @param array<string,mixed> $checkpoint Current checkpoint.
54 * @param array<string,mixed> $step_context Authorized presentation context.
55 *
56 * @return string Translated action label.
57 */
58 public function get_primary_action_label( array $checkpoint, array $step_context = array() ) {
59 $mappings = $this->get_service_mappings( $checkpoint );
60 $services = isset( $checkpoint['values']['services']['services'] ) && is_array( $checkpoint['values']['services']['services'] )
61 ? $checkpoint['values']['services']['services']
62 : array();
63 if ( empty( $services ) && isset( $step_context['services'] ) && is_array( $step_context['services'] ) ) {
64 $services = $step_context['services'];
65 }
66
67 foreach ( $services as $service ) {
68 $draft_id = isset( $service['draft_id'] ) ? sanitize_key( (string) $service['draft_id'] ) : '';
69 $source_id = isset( $service['source_service_id'] ) ? absint( $service['source_service_id'] ) : 0;
70 if ( ! $source_id && ( '' === $draft_id || empty( $mappings[ $draft_id ] ) ) ) {
71 return __( 'Create services & continue', 'booking' );
72 }
73 }
74
75 return __( 'Update services & continue', 'booking' );
76 }
77
78 /**
79 * Revalidate and persist a bounded Services collection.
80 *
81 * Existing Provider assignments, Booking Form assignments, and metadata are
82 * retained because the Services editor does not own those fields. A database
83 * transaction is used when available; explicit before-images provide bounded
84 * compensation for storage engines that do not support transactions.
85 *
86 * @param array<string,mixed> $validated_fields Validated current-step fields.
87 * @param array<string,mixed> $checkpoint Current checkpoint.
88 * @param array<string,mixed> $operation_context Stable operation metadata.
89 *
90 * @return array<string,mixed>|WP_Error Verified result or error.
91 */
92 public function save( array $validated_fields, array $checkpoint, array $operation_context ) {
93 if ( 'guided_appointment_flow' !== $this->get_customer_journey_id( $checkpoint ) ) {
94 return new WP_Error( 'wpbc_setup_wizard_services_journey_invalid', __( 'Services can be saved here only for the Guided appointment flow.', 'booking' ) );
95 }
96 if (
97 ! function_exists( 'wpbc_appointment_services_get_manage_capability' )
98 || ! current_user_can( wpbc_appointment_services_get_manage_capability() )
99 || ! function_exists( 'wpbc_appointment_services_get_data_provider' )
100 ) {
101 return new WP_Error( 'wpbc_setup_wizard_services_forbidden', __( 'You are not allowed to manage Appointment Services.', 'booking' ) );
102 }
103
104 $submitted_services = isset( $validated_fields['services'] ) ? $validated_fields['services'] : array();
105 $submitted_services = $this->services->validate_services( $submitted_services, true );
106 if ( is_wp_error( $submitted_services ) ) {
107 return $submitted_services;
108 }
109 $inactive_service_ids = isset( $validated_fields['inactive_service_ids'] ) ? $validated_fields['inactive_service_ids'] : array();
110 $inactive_service_ids = $this->services->validate_inactive_service_ids( $inactive_service_ids );
111 if ( is_wp_error( $inactive_service_ids ) ) {
112 return $inactive_service_ids;
113 }
114
115 $repository = wpbc_appointment_services_get_data_provider();
116 $required_methods = array( 'begin_transaction', 'commit_transaction', 'rollback_transaction', 'find', 'save', 'find_by_setup_operation', 'get_deletion_before_image', 'update_details_preserving_assignments', 'move_to_draft_preserving_assignments', 'restore_service_row', 'compare_and_delete_service' );
117 if ( ! is_object( $repository ) ) {
118 return new WP_Error( 'wpbc_setup_wizard_services_storage_unsupported', __( 'The configured Appointment Services storage does not support Setup Wizard saves.', 'booking' ) );
119 }
120 foreach ( $required_methods as $required_method ) {
121 if ( ! method_exists( $repository, $required_method ) ) {
122 return new WP_Error( 'wpbc_setup_wizard_services_storage_unsupported', __( 'The configured Appointment Services storage does not support Setup Wizard saves.', 'booking' ) );
123 }
124 }
125 $operation_id = isset( $operation_context['operation_id'] ) ? sanitize_key( (string) $operation_context['operation_id'] ) : '';
126 $service_mappings = $this->get_service_mappings( $checkpoint );
127 $default_provider = $this->get_default_provider_id();
128 $before_images = array();
129 $created_images = array();
130 $saved_services = array();
131 $created_ids = array();
132 $updated_ids = array();
133 $inactive_count = 0;
134 $transaction = $repository->begin_transaction();
135 $submitted_service_ids = $this->get_submitted_service_ids( $submitted_services, $service_mappings );
136 if ( array_intersect( $inactive_service_ids, $submitted_service_ids ) ) {
137 $this->compensate( $repository, $transaction, $before_images, $created_images );
138 return new WP_Error( 'wpbc_setup_wizard_service_status_conflict', __( 'A Service cannot remain active and move to Draft in the same save.', 'booking' ) );
139 }
140 $allowed_inactive_service_ids = $this->get_allowed_inactive_service_ids( $checkpoint );
141
142 foreach ( $submitted_services as $service_draft ) {
143 $draft_id = sanitize_key( (string) $service_draft['draft_id'] );
144 $service_id = absint( $service_draft['source_service_id'] );
145 $is_operation_retry = false;
146 if ( ! $service_id && isset( $service_mappings[ $draft_id ] ) ) {
147 $service_id = absint( $service_mappings[ $draft_id ] );
148 }
149 if ( ! $service_id && '' !== $operation_id ) {
150 $retry_service = $repository->find_by_setup_operation( $operation_id, $draft_id );
151 if ( is_wp_error( $retry_service ) ) {
152 $this->compensate( $repository, $transaction, $before_images, $created_images );
153 return new WP_Error( 'wpbc_setup_wizard_service_retry_not_verified', __( 'A previous Service save could not be verified safely. Reload the Services step and try again.', 'booking' ) );
154 }
155 if ( is_array( $retry_service ) ) {
156 $service_id = absint( $retry_service['service_id'] );
157 $is_operation_retry = true;
158 }
159 }
160
161 $existing_service = array();
162 if ( $service_id ) {
163 $before_image = $repository->get_deletion_before_image( $service_id );
164 if ( is_wp_error( $before_image ) || ! $this->is_current_owner_service( $before_image ) ) {
165 $this->compensate( $repository, $transaction, $before_images, $created_images );
166 return new WP_Error( 'wpbc_setup_wizard_service_stale', __( 'A Service changed or is no longer available. Reload the Services step and try again.', 'booking' ) );
167 }
168 $before_images[ $service_id ] = $before_image;
169 $existing_service = $before_image['service'];
170 $existing_service['resource_ids'] = wp_list_pluck( $before_image['assignments'], 'resource_id' );
171 }
172
173 $service_payload = array_merge(
174 $existing_service,
175 array(
176 'service_id' => $service_id,
177 'title' => $service_draft['title'],
178 'description' => $service_draft['description'],
179 'picture_url' => $service_draft['picture_url'],
180 'duration_minutes' => $service_draft['duration_minutes'],
181 'buffer_before_minutes' => $service_draft['buffer_before_minutes'],
182 'buffer_after_minutes' => $service_draft['buffer_after_minutes'],
183 'base_cost' => $service_draft['base_cost'],
184 'booking_form_id' => isset( $existing_service['booking_form_id'] ) ? absint( $existing_service['booking_form_id'] ) : 0,
185 'resource_ids' => ! empty( $existing_service['resource_ids'] ) ? $existing_service['resource_ids'] : array( $default_provider ),
186 'status' => 'active',
187 'metadata' => $service_id
188 ? ( isset( $existing_service['metadata'] ) ? $existing_service['metadata'] : array() )
189 : array(
190 'wpbc_setup_wizard_operation_id' => $operation_id,
191 'wpbc_setup_wizard_draft_id' => $draft_id,
192 ),
193 )
194 );
195
196 if ( ! $service_id && ! $default_provider ) {
197 $was_restored = $this->compensate( $repository, $transaction, $before_images, $created_images );
198 $message = $was_restored
199 ? __( 'A Service cannot be created until an authorized Provider is available.', 'booking' )
200 : __( 'A Service cannot be created because no authorized Provider is available, and previous Service values could not be restored completely.', 'booking' );
201 return new WP_Error( 'wpbc_setup_wizard_service_provider_missing', $message );
202 }
203
204 $saved_service = $service_id
205 ? $repository->update_details_preserving_assignments( $service_payload )
206 : $repository->save( $service_payload );
207 if ( is_wp_error( $saved_service ) ) {
208 $was_restored = $this->compensate( $repository, $transaction, $before_images, $created_images );
209 $message = $was_restored
210 ? __( 'A Service could not be saved. Previous Service values were restored.', 'booking' )
211 : __( 'A Service could not be saved, and its previous values could not be restored completely. Review Appointment Services before continuing.', 'booking' );
212 return new WP_Error( 'wpbc_setup_wizard_service_not_saved', $message );
213 }
214
215 $saved_id = absint( $saved_service['service_id'] );
216 if ( ! $service_id ) {
217 $created_image = $repository->get_deletion_before_image( $saved_id );
218 if ( is_wp_error( $created_image ) ) {
219 $this->compensate( $repository, $transaction, $before_images, $created_images );
220 return new WP_Error( 'wpbc_setup_wizard_service_not_verified', __( 'A newly created Service could not be verified safely.', 'booking' ) );
221 }
222 $created_images[ $saved_id ] = $created_image;
223 $created_ids[] = $this->format_service_mapping( $draft_id, $saved_id );
224 } elseif ( $is_operation_retry ) {
225 $created_ids[] = $this->format_service_mapping( $draft_id, $saved_id );
226 } else {
227 $updated_ids[] = $this->format_service_mapping( $draft_id, $saved_id );
228 }
229
230 $saved_services[] = $this->get_service_fingerprint_values( $saved_service );
231 }
232
233 foreach ( $inactive_service_ids as $inactive_service_id ) {
234 $before_image = $repository->get_deletion_before_image( $inactive_service_id );
235 if ( is_wp_error( $before_image ) || ! $this->is_current_owner_service( $before_image ) ) {
236 $this->compensate( $repository, $transaction, $before_images, $created_images );
237 return new WP_Error( 'wpbc_setup_wizard_service_draft_stale', __( 'A Service selected for Draft changed or is no longer available. Reload the Services step and try again.', 'booking' ) );
238 }
239
240 $existing_service = $before_image['service'];
241 $is_status_retry = $this->is_inactive_operation_retry( $existing_service, $operation_id );
242 if ( ! in_array( $inactive_service_id, $allowed_inactive_service_ids, true ) && ! $is_status_retry ) {
243 $this->compensate( $repository, $transaction, $before_images, $created_images );
244 return new WP_Error( 'wpbc_setup_wizard_service_draft_forbidden', __( 'A Service selected for Draft is not part of this saved Setup Wizard state.', 'booking' ) );
245 }
246
247 $existing_status = isset( $existing_service['status'] ) ? sanitize_key( (string) $existing_service['status'] ) : '';
248 if ( 'inactive' === $existing_status ) {
249 $updated_ids[] = $this->format_inactive_service_id( $inactive_service_id );
250 $saved_services[] = $this->get_service_fingerprint_values( $existing_service );
251 ++$inactive_count;
252 continue;
253 }
254 if ( 'active' !== $existing_status ) {
255 $this->compensate( $repository, $transaction, $before_images, $created_images );
256 return new WP_Error( 'wpbc_setup_wizard_service_draft_status_invalid', __( 'Only an active Service can be moved to Draft from Setup Wizard.', 'booking' ) );
257 }
258
259 $before_images[ $inactive_service_id ] = $before_image;
260
261 $saved_service = $repository->move_to_draft_preserving_assignments( $inactive_service_id, $operation_id );
262 if (
263 is_wp_error( $saved_service )
264 || 'inactive' !== sanitize_key( isset( $saved_service['status'] ) ? (string) $saved_service['status'] : '' )
265 ) {
266 $was_restored = $this->compensate( $repository, $transaction, $before_images, $created_images );
267 $message = $was_restored
268 ? __( 'A Service could not be moved to Draft. Previous Service values were restored.', 'booking' )
269 : __( 'A Service could not be moved to Draft, and previous values could not be restored completely. Review Appointment Services before continuing.', 'booking' );
270 return new WP_Error( 'wpbc_setup_wizard_service_draft_not_saved', $message );
271 }
272
273 $updated_ids[] = $this->format_inactive_service_id( $inactive_service_id );
274 $saved_services[] = $this->get_service_fingerprint_values( $saved_service );
275 ++$inactive_count;
276 }
277
278 if ( $transaction && ! $repository->commit_transaction() ) {
279 $was_restored = $this->compensate( $repository, true, $before_images, $created_images );
280 $message = $was_restored
281 ? __( 'Services could not be committed. Previous Service values were restored.', 'booking' )
282 : __( 'Services could not be committed or restored completely. Review Appointment Services before continuing.', 'booking' );
283 return new WP_Error( 'wpbc_setup_wizard_services_commit_failed', $message );
284 }
285
286 $summary = empty( $created_ids ) ? __( 'Appointment Services updated.', 'booking' ) : __( 'Appointment Services created and updated.', 'booking' );
287 if ( 1 === $inactive_count ) {
288 $summary = __( 'Appointment Services updated; one Service moved to Draft.', 'booking' );
289 } elseif ( 1 < $inactive_count ) {
290 /* translators: %d: Number of Appointment Services moved to Draft. */
291 $summary = sprintf( __( 'Appointment Services updated; %d Services moved to Draft.', 'booking' ), $inactive_count );
292 }
293
294 return array(
295 'status' => 'saved',
296 'summary' => $summary,
297 'canonical_fingerprint' => hash( 'sha256', wp_json_encode( $saved_services ) ),
298 'created_ids' => $created_ids,
299 'updated_ids' => $updated_ids,
300 'warnings' => array(),
301 'writes_canonical_settings' => true,
302 );
303 }
304
305 /**
306 * Resolve canonical Service IDs retained by the active proposal collection.
307 *
308 * @param array<int,array<string,mixed>> $submitted_services Validated Service proposals.
309 * @param array<string,int> $service_mappings Retained draft-to-Service mappings.
310 *
311 * @return int[] Canonical Service IDs that must remain active.
312 */
313 private function get_submitted_service_ids( array $submitted_services, array $service_mappings ) {
314 $service_ids = array();
315 foreach ( $submitted_services as $submitted_service ) {
316 $draft_id = sanitize_key( (string) $submitted_service['draft_id'] );
317 $service_id = absint( $submitted_service['source_service_id'] );
318 if ( ! $service_id && isset( $service_mappings[ $draft_id ] ) ) {
319 $service_id = absint( $service_mappings[ $draft_id ] );
320 }
321 if ( $service_id ) {
322 $service_ids[] = $service_id;
323 }
324 }
325
326 return array_values( array_unique( $service_ids ) );
327 }
328
329 /**
330 * Build the owner-authorized allow-list for an explicit Draft transition.
331 *
332 * Current active canonical IDs authorize the initial action. Previously saved
333 * checkpoint values authorize idempotent revisits after the Service is already
334 * inactive and therefore no longer appears in the active repository query.
335 *
336 * @param array<string,mixed> $checkpoint Current checkpoint.
337 *
338 * @return int[] Authorized Service IDs.
339 */
340 private function get_allowed_inactive_service_ids( array $checkpoint ) {
341 $service_ids = $this->services->get_authorized_active_service_ids();
342 $stored_ids = isset( $checkpoint['values']['services']['inactive_service_ids'] ) && is_array( $checkpoint['values']['services']['inactive_service_ids'] )
343 ? $checkpoint['values']['services']['inactive_service_ids']
344 : array();
345
346 return array_values( array_unique( array_filter( array_map( 'absint', array_merge( $service_ids, $stored_ids ) ) ) ) );
347 }
348
349 /**
350 * Confirm that an inactive Service belongs to the current retry operation.
351 *
352 * @param array<string,mixed> $service Canonical Service row.
353 * @param string $operation_id Stable progressive-save operation ID.
354 *
355 * @return bool True when the stored marker proves an idempotent retry.
356 */
357 private function is_inactive_operation_retry( array $service, $operation_id ) {
358 if ( '' === $operation_id || 'inactive' !== sanitize_key( isset( $service['status'] ) ? (string) $service['status'] : '' ) ) {
359 return false;
360 }
361
362 $metadata = $this->decode_service_metadata( isset( $service['metadata'] ) ? $service['metadata'] : array() );
363
364 return isset( $metadata['wpbc_setup_wizard_inactive_operation_id'] )
365 && hash_equals( $operation_id, (string) $metadata['wpbc_setup_wizard_inactive_operation_id'] );
366 }
367
368 /**
369 * Decode canonical Service metadata without assuming the native provider.
370 *
371 * @param mixed $metadata Canonical metadata array or JSON string.
372 *
373 * @return array<string,mixed> Decoded metadata.
374 */
375 private function decode_service_metadata( $metadata ) {
376 if ( function_exists( 'wpbc_appointment_services_decode_metadata' ) ) {
377 return wpbc_appointment_services_decode_metadata( $metadata );
378 }
379 if ( is_array( $metadata ) ) {
380 return $metadata;
381 }
382
383 $decoded = json_decode( (string) $metadata, true );
384
385 return is_array( $decoded ) ? $decoded : array();
386 }
387
388 /**
389 * Format a stable result marker for one Service moved to Draft.
390 *
391 * @param int $service_id Canonical Service ID.
392 *
393 * @return string Step-result marker.
394 */
395 private function format_inactive_service_id( $service_id ) {
396 return 'service_status:' . absint( $service_id ) . ':inactive';
397 }
398
399 /**
400 * Restore changed records or roll back the active transaction.
401 *
402 * @param WPBC_Appointment_Services_Repository $repository Canonical repository.
403 * @param bool $transaction Whether a transaction is active.
404 * @param array<int,array<string,mixed>> $before_images Existing Service before-images.
405 * @param array<int,array<string,mixed>> $created_images Created Service before-images.
406 *
407 * @return bool True when the rollback or every compensation write succeeds.
408 */
409 private function compensate( $repository, $transaction, array $before_images, array $created_images ) {
410 if ( $transaction ) {
411 $repository->rollback_transaction();
412 $rollback_verified = true;
413 foreach ( $before_images as $service_id => $before_image ) {
414 $current_image = $repository->get_deletion_before_image( $service_id );
415 if ( is_wp_error( $current_image ) || $before_image !== $current_image ) {
416 $rollback_verified = false;
417 break;
418 }
419 }
420 if ( $rollback_verified ) {
421 foreach ( array_keys( $created_images ) as $service_id ) {
422 if ( ! is_wp_error( $repository->find( $service_id ) ) ) {
423 $rollback_verified = false;
424 break;
425 }
426 }
427 }
428 if ( $rollback_verified ) {
429 return true;
430 }
431 }
432
433 $was_restored = true;
434 foreach ( array_reverse( $created_images, true ) as $created_image ) {
435 if ( is_wp_error( $repository->compare_and_delete_service( $created_image ) ) ) {
436 $was_restored = false;
437 }
438 }
439 foreach ( $before_images as $before_image ) {
440 if ( is_wp_error( $repository->restore_service_row( $before_image ) ) ) {
441 $was_restored = false;
442 }
443 }
444
445 return $was_restored;
446 }
447
448 /**
449 * Return the first authorized Provider used for a new Service.
450 *
451 * @return int Provider resource ID.
452 */
453 private function get_default_provider_id() {
454 $provider_options = function_exists( 'wpbc_appointment_services_get_provider_options' )
455 ? wpbc_appointment_services_get_provider_options()
456 : array();
457 $provider_ids = array_values( array_filter( array_map( 'absint', array_keys( (array) $provider_options ) ) ) );
458
459 if ( ! empty( $provider_ids ) ) {
460 return (int) reset( $provider_ids );
461 }
462
463 return 0;
464 }
465
466 /**
467 * Confirm that a Service before-image belongs to the effective wizard owner.
468 *
469 * Catalog super administrators may read all MultiUser owners, but a Setup
470 * Wizard session must mutate only its effective owner context.
471 *
472 * @param array<string,mixed> $before_image Canonical Service before-image.
473 *
474 * @return bool True when the Service owner matches the wizard owner.
475 */
476 private function is_current_owner_service( array $before_image ) {
477 $service = isset( $before_image['service'] ) && is_array( $before_image['service'] ) ? $before_image['service'] : array();
478 $owner_user_id = function_exists( 'wpbc_appointment_services_get_owner_user_id' )
479 ? absint( wpbc_appointment_services_get_owner_user_id() )
480 : 0;
481
482 return isset( $service['owner_user_id'] ) && $owner_user_id === absint( $service['owner_user_id'] );
483 }
484
485 /**
486 * Extract canonical Service IDs from retained step-result metadata.
487 *
488 * @param array<string,mixed> $checkpoint Current checkpoint.
489 *
490 * @return array<string,int> Service IDs keyed by draft identifier.
491 */
492 private function get_service_mappings( array $checkpoint ) {
493 $result = isset( $checkpoint['step_results']['services'] ) && is_array( $checkpoint['step_results']['services'] ) ? $checkpoint['step_results']['services'] : array();
494 $stored = array_merge( isset( $result['created_ids'] ) ? (array) $result['created_ids'] : array(), isset( $result['updated_ids'] ) ? (array) $result['updated_ids'] : array() );
495 $mappings = array();
496
497 foreach ( $stored as $mapping ) {
498 if ( is_scalar( $mapping ) && preg_match( '/^service:([a-z0-9_-]{1,64}):([0-9]+)$/', (string) $mapping, $matches ) ) {
499 $mappings[ $matches[1] ] = absint( $matches[2] );
500 }
501 }
502
503 return $mappings;
504 }
505
506 /**
507 * Format one bounded Service identity mapping.
508 *
509 * @param string $draft_id Stable wizard draft identifier.
510 * @param int $service_id Canonical Service ID.
511 *
512 * @return string Mapping stored in step-result metadata.
513 */
514 private function format_service_mapping( $draft_id, $service_id ) {
515 return 'service:' . sanitize_key( $draft_id ) . ':' . absint( $service_id );
516 }
517
518 /**
519 * Return stable canonical fields used for post-write fingerprinting.
520 *
521 * @param array<string,mixed> $service Canonical Service row.
522 *
523 * @return array<string,mixed> Stable fields.
524 */
525 private function get_service_fingerprint_values( array $service ) {
526 return array(
527 'service_id' => absint( $service['service_id'] ),
528 'title' => (string) $service['title'],
529 'description' => (string) $service['description'],
530 'duration_minutes' => absint( $service['duration_minutes'] ),
531 'buffer_before_minutes' => absint( $service['buffer_before_minutes'] ),
532 'buffer_after_minutes' => absint( $service['buffer_after_minutes'] ),
533 'base_cost' => (string) $service['base_cost'],
534 'booking_form_id' => absint( $service['booking_form_id'] ),
535 'resource_ids' => array_values( array_map( 'absint', (array) $service['resource_ids'] ) ),
536 'status' => (string) $service['status'],
537 );
538 }
539
540 /**
541 * Read the saved Customer Journey.
542 *
543 * @param array<string,mixed> $checkpoint Current checkpoint.
544 *
545 * @return string Journey ID.
546 */
547 private function get_customer_journey_id( array $checkpoint ) {
548 return isset( $checkpoint['values']['customer_journey']['customer_journey'] )
549 ? sanitize_key( (string) $checkpoint['values']['customer_journey']['customer_journey'] )
550 : '';
551 }
552
553 /**
554 * Services do not complete the route.
555 *
556 * @return bool False.
557 */
558 public function is_terminal() {
559 return false;
560 }
561 }
562