PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.0
2.5.0 2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 All 34 releases
← All changes | app/Modules/MCP/Tools/BookingWriteTools.php +35 -79 2.4.0 → 2.5.0 View file →
@@ -15,41 +15,28 @@
15 15
16 16 defined('ABSPATH') || exit;
17 17
18 18 /**
19 - * The two tools that change something.
19 + * The write tools. `create-booking` has its own parameter shape; everything
20 + * acting on an existing booking goes through `manage-booking` behind an
21 + * `action` enum.
20 22 *
21 - * `create-booking` stands alone because its parameter shape — attendee details,
22 - * custom fields, guests, location — shares nothing with the others. Everything
23 - * that acts on a booking that already exists goes through `manage-booking`
24 - * behind an `action` enum, which is where Cal.com spends six separate tools.
23 + * Destructive calls need a confirm_token from a dry run (see WriteGuard).
24 + * Whether a call is destructive is decided per call, see needsConfirmation().
25 25 *
26 - * Destructive actions will not execute without a confirm_token minted by a dry
27 - * run. The token is bound to BOTH a fingerprint of the booking's current state
28 - * — so a booking that moved while the agent was thinking cannot be acted on
29 - * with stale numbers — AND a digest of the parameters that were previewed, so
30 - * the change that executes is the change a human approved. Reversible actions
31 - * (complete, no_show, resend_email, and update_details on anything but the
32 - * email address) execute directly.
33 - *
34 - * "Destructive" is decided per call rather than per action name, because two of
35 - * them are only destructive sometimes. See needsConfirmation().
36 - *
37 26 * @see \FluentBooking\App\Modules\MCP\Support\WriteGuard for the contract.
38 27 */
39 28 class BookingWriteTools
40 29 {
41 30 /**
42 - * Actions that must be previewed before they can execute. These either
43 - * cannot be undone from the agent's side (cancel, reject) or move a real
44 - * person's calendar entry (create, reschedule).
31 + * manage-booking actions that always need a preview: they can't be undone
32 + * (cancel, reject) or move a real person's calendar entry (reschedule).
45 33 */
46 34 const DESTRUCTIVE_ACTIONS = ['reschedule', 'cancel', 'reject'];
47 35
48 36 /**
49 - * Ceiling on `guests`. Enforced in the schema so an oversized payload is
50 - * refused before WordPress decodes it and the sanitizer walks every entry —
51 - * the per-event seat limit further down only applies after all that work.
37 + * Ceiling on `guests`, enforced in the schema so an oversized payload is
38 + * refused before the sanitizer walks it. The seat limit applies later.
52 39 */
53 40 const MAX_GUESTS = 50;
54 41
55 42 public static function definitions()
@@ -252,12 +239,10 @@
252 239 }
253 240
254 241 $tool = 'fluent-booking/create-booking';
255 242 $timezone = MCPHelper::resolveTimezone(Arr::get($params, 'timezone', ''));
256 - // A create has no existing entity, so the token is bound to the exact
257 - // slot being claimed. Two agents previewing the same slot mint separate
258 - // user-scoped tokens; the availability re-check at execute time is what
259 - // stops the second one from double-booking.
243 + // No existing entity, so bind the token to the slot being claimed. The
244 + // availability re-check at execute time prevents double-booking.
260 245 $entityKey = 'event:' . $eventId . ':' . Arr::get($params, 'start_time', '') . ':' . strtolower((string) Arr::get($params, 'email', ''));
261 246
262 247 $digest = WriteGuard::paramsDigest($params);
263 248
@@ -274,13 +259,10 @@
274 259 WriteGuard::CONFIRM_NEXT_STEP
275 260 );
276 261 }
277 262
278 - // idempotent() OUTSIDE confirm(), not the other way round. confirm()
279 - // consumes the token, so with the old ordering the retry-after-timeout
280 - // this key exists to absorb was rejected as `confirmation_expired`
281 - // before the recorded result was ever consulted — and the agent's
282 - // recovery path was a fresh dry_run and a second booking.
263 + // idempotent() must wrap confirm(): confirm() consumes the token, so a
264 + // retry would otherwise fail before reaching the recorded result.
283 265 return WriteGuard::idempotent($tool, $entityKey, Arr::get($params, 'idempotency_key', ''), function () use ($tool, $entityKey, $event, $params, $timezone, $digest) {
284 266 $confirmed = WriteGuard::confirm($tool, $entityKey, self::createFingerprint($event), Arr::get($params, 'confirm_token', ''), $digest);
285 267
286 268 if (is_wp_error($confirmed)) {
@@ -297,11 +279,9 @@
297 279 'created' => true,
298 280 'booking' => BookingProjector::full($booking, $timezone),
299 281 ];
300 282
301 - // A guest the caller asked for and did not get has to be named. An
302 - // agent told `created: true` for a four-person booking that seated
303 - // one otherwise reports a wrong number as a right one.
283 + // Name any requested guest who wasn't seated.
304 284 if ($dropped = BookingWriter::droppedGuests()) {
305 285 $data['guests_dropped'] = $dropped;
306 286 }
307 287
@@ -364,11 +344,9 @@
364 344 return $preview;
365 345 }
366 346
367 347 if (!$needsConfirmation) {
368 - // Reversible actions still honour dry_run, because an agent that
369 - // previews everything by habit should not be punished for it —
370 - // it just does not need a token to follow up.
348 + // Reversible actions still honour dry_run; they just need no token.
371 349 return MCPHelper::success(
372 350 ['dry_run' => true, 'preview' => $preview],
373 351 ['timezone' => $timezone],
374 352 'Nothing was changed. This action is reversible — call again without dry_run to apply it; no confirm_token is needed.'
@@ -381,10 +359,9 @@
381 359 WriteGuard::CONFIRM_NEXT_STEP
382 360 );
383 361 }
384 362
385 - // See createBooking(): the idempotency wrapper has to sit OUTSIDE the
386 - // confirm-token check, because the check is one-shot.
363 + // idempotent() wraps confirm(), see createBooking().
387 364 return WriteGuard::idempotent($tool, $entityKey, Arr::get($params, 'idempotency_key', ''), function () use ($tool, $entityKey, $booking, $action, $params, $timezone, $digest, $needsConfirmation) {
388 365 if ($needsConfirmation) {
389 366 $confirmed = WriteGuard::confirm(
390 367 $tool,
@@ -405,15 +382,11 @@
405 382 });
406 383 }
407 384
408 385 /**
409 - * Rebuild a write's response from the reference the idempotency record
410 - * keeps, reading the booking as it stands now.
386 + * Rebuild a write's response from the idempotency record's booking id,
387 + * reading the booking as it stands now. See WriteGuard::idempotent().
411 388 *
412 - * The record itself holds ids only — see WriteGuard::idempotent() — so a
413 - * replay re-projects rather than handing back a day-old copy of the
414 - * attendee's details.
415 - *
416 389 * @param array $ref
417 390 * @param string $timezone
418 391 * @param array $extra
419 392 *
@@ -439,24 +412,16 @@
439 412 );
440 413 }
441 414
442 415 /**
443 - * Whether this particular call has to be previewed and confirmed first.
416 + * Whether this call has to be previewed and confirmed first. Besides
417 + * DESTRUCTIVE_ACTIONS, two cases depend on the parameters:
444 418 *
445 - * Most of the answer is the action name, but two cases are only destructive
446 - * depending on what is being asked, and both were previously waved through
447 - * as "reversible":
419 + * - `update_details` changing `email`: future emails, join link included,
420 + * go to the new address without the attendee being told.
421 + * - `confirm` with an unsettled payment order: it marks the order paid and
422 + * fires the payment-completed hooks.
448 423 *
449 - * - `update_details` changing `email`. Reversible in the database and not
450 - * reversible anywhere else: the address becomes the delivery target for
451 - * `resend_email`, which carries the meeting join link and lands at the
452 - * new address without the real attendee hearing about it. Rewriting a
453 - * booking's contact address is not an edit, it is a redirection.
454 - * - `confirm` on a booking with an unsettled payment order. It marks the
455 - * order paid and fires the payment-completed hooks. Nothing about a
456 - * booking's money state should move without the operator seeing it
457 - * first.
458 - *
459 424 * @param Booking $booking
460 425 * @param string $action
461 426 * @param array $params
462 427 * @return bool
@@ -521,17 +486,16 @@
521 486 );
522 487 }
523 488
524 489 /**
525 - * What a create would do, without doing it. Deliberately names every
526 - * recipient: the operator reading the agent's transcript should be able to
527 - * see who is about to be emailed before approving.
490 + * What a create would do, without doing it. Includes who would be emailed,
491 + * so the operator sees it before approving.
528 492 *
529 493 * @return array|\WP_Error
530 494 */
531 495 private static function previewCreate(CalendarSlot $event, $params, $timezone)
532 496 {
533 - // The same checks the execute runs, so "the dry run worked" means something.
497 + // Same checks as execute, so a passing dry run means something.
534 498 $valid = BookingWriter::validateCreate($event, $params);
535 499
536 500 if (is_wp_error($valid)) {
537 501 return $valid;
@@ -573,14 +537,11 @@
573 537 return $preview;
574 538 }
575 539
576 540 /**
577 - * Whether the requested slot is free, for the preview only.
541 + * Whether the requested slot is free, for the preview only. Advisory: true
542 + * means "free a moment ago", not a reservation. null when unknown.
578 543 *
579 - * Advisory: the slot is claimed and re-checked under a lock at execute
580 - * time, so true means "free a moment ago", never a reservation. null when
581 - * the engine could not answer.
582 - *
583 544 * @return bool|null
584 545 */
585 546 private static function previewSlotAvailability(CalendarSlot $event, $params, $timezone)
586 547 {
@@ -615,11 +576,10 @@
615 576 * @return array|\WP_Error
616 577 */
617 578 private static function previewAction(Booking $booking, $action, $params, $timezone)
618 579 {
619 - // The same state-machine check the execute runs. Without it a dry run
620 - // previewed `no_show -> cancelled` and minted a confirm_token for a
621 - // call the execute would refuse.
580 + // Same state-machine check as execute, so we never mint a token for a
581 + // transition execute would refuse.
622 582 $transitions = BookingWriter::statusTransitions();
623 583
624 584 if (isset($transitions[$action])) {
625 585 $target = $transitions[$action]['to'];
@@ -727,12 +687,10 @@
727 687 return $preview;
728 688 }
729 689
730 690 /**
731 - * Who an action would email, described rather than enumerated — the exact
732 - * template that fires depends on the event type's notification settings,
733 - * and listing every address would leak contact details into a preview an
734 - * agent may echo back verbatim.
691 + * Who an action would email, as roles rather than addresses, so a preview
692 + * the agent may echo back doesn't leak contact details.
735 693 *
736 694 * @return array
737 695 */
738 696 private static function recipientSummary(CalendarSlot $event, $booking = null)
@@ -754,12 +712,10 @@
754 712 return $recipients;
755 713 }
756 714
757 715 /**
758 - * A create has no prior state to go stale, but the event type's own
759 - * configuration does — an event deactivated or re-timed between preview and
760 - * execute should invalidate the token rather than silently book against the
761 - * old shape.
716 + * Fingerprint the event type, so deactivating or re-timing it between
717 + * preview and execute invalidates the token.
762 718 *
763 719 * @return string
764 720 */
765 721 private static function createFingerprint(CalendarSlot $event)