settings = $settings; $this->states = new StateService(); } /** * Add Booking * * @param Draft $draft * @param Transaction $transaction * @param mixed[] $context * @return int */ public function add(Draft $draft, Transaction $transaction, array $context = []): int { $this->last_error = ''; $locks = new CapacityLocks(); $preserve_resource_assignments = !empty($context['allow_conflicts']) || !empty($context['preserve_resource_assignments']); if (!$locks->acquire($draft, 5, $preserve_resource_assignments)) { $this->last_error = esc_html__('The selected resource is being booked by another request. Please try again.', 'daytidy'); return 0; } try { return $this->addUnlocked($draft, $transaction, $context); } finally { $locks->release(); } } public function lastError(): string { return $this->last_error; } /** * @param mixed[] $context */ private function addUnlocked(Draft $draft, Transaction $transaction, array $context = []): int { /** * No Duplicate */ $db = new Db(); $booking_ids = $db->select($db->prepare( "SELECT `post_id` FROM `#__postmeta` WHERE `meta_key` = %s AND `meta_value` = %s", 'daytidy_ref', $draft->getId() )); foreach ($booking_ids as $index => $booking_id) { $status = get_post_status($booking_id->post_id); if (!$status || $status === Statuses::TRASH) unset($booking_ids[$index]); } // Already Done if (count($booking_ids)) return 0; $resource_conflict = $draft->getResourceConflictMessage(); if ($resource_conflict && empty($context['allow_conflicts'])) { $this->last_error = $resource_conflict; return 0; } /** * Booking Config */ $config = $this->settings->module('booking'); /** * Data */ $offering = $draft->getOffering(); $form = $draft->getMappedForm(); $registration = $draft->getRegistration(); $raw_form = $draft->getForm(); $first_name = $form['first_name']['value'] ?? $raw_form['_manual_first_name'] ?? ''; $last_name = $form['last_name']['value'] ?? $raw_form['_manual_last_name'] ?? ''; $email = $form['email']['value'] ?? $raw_form['_manual_email'] ?? ''; $email = $email ? wp_strip_all_tags((string) $email) : ''; /** * User Registration */ $customer_id = Customer::register([ 'first_name' => $first_name, 'last_name' => $last_name, 'email' => $email, 'username' => $registration['username'] ?? '', 'password' => $registration['password'] ?? '', 'wp_registration' => isset($config['registration']) && $config['registration'], 'form' => $form, ]); if ($customer_id <= 0) { $this->last_error = esc_html__('Customer account could not be created. Please review the registration details and try again.', 'daytidy'); return 0; } /** * Create Booking */ $booking_id = wp_insert_post([ 'post_author' => $customer_id, 'post_type' => \DayTidy\Posts\Booking::PT, 'post_title' => $email.' - '.trim($first_name .' '.$last_name), 'post_content' => $draft->getId(), 'post_date' => wp_date(DateTime::tech(), strtotime($draft->getFirstSession())), 'post_status' => Statuses::PUBLISH, ]); // Meta Data update_post_meta($booking_id, 'daytidy_customer_id', $customer_id); update_post_meta($booking_id, 'daytidy_email', $email); update_post_meta($booking_id, 'daytidy_first_name', $first_name); update_post_meta($booking_id, 'daytidy_last_name', $last_name); update_post_meta($booking_id, 'daytidy_offering_id', $offering->id()); update_post_meta($booking_id, 'daytidy_ref', $draft->getId()); update_post_meta($booking_id, 'daytidy_form', $draft->getForm()); update_post_meta($booking_id, 'daytidy_participants', $draft->getParticipantRows()); update_post_meta($booking_id, 'daytidy_resources', $draft->getResources()); update_post_meta($booking_id, 'daytidy_coupon', $draft->getCoupon()); update_post_meta($booking_id, 'daytidy_total', $transaction->getTotal()); update_post_meta($booking_id, 'daytidy_pricing', $transaction->getPricing()); update_post_meta($booking_id, 'daytidy_gateway_ref_id', $transaction->getGatewayRefId()); if (!$this->states->initialize($booking_id, $context)) { $this->last_error = $this->states->lastError(); wp_delete_post($booking_id, true); return 0; } update_post_meta($booking_id, 'daytidy_session', $draft->getFirstSession()); update_post_meta($booking_id, 'daytidy_key', md5(time().wp_rand(100000, 999999))); update_post_meta($booking_id, 'daytidy_capacity_units', $draft->getTotalSeats()); update_post_meta($booking_id, 'daytidy_submitted_at', current_time('Y-m-d H:i:s', true)); update_post_meta($booking_id, 'daytidy_source', sanitize_key((string) ($context['source'] ?? 'frontend'))); if (!empty($context['allow_conflicts'])) { update_post_meta($booking_id, 'daytidy_manual_override', [ 'user_id' => get_current_user_id(), 'created_at' => current_time('mysql', true), 'reason' => sanitize_textarea_field((string) ($context['override_reason'] ?? '')), 'warnings' => isset($context['warnings']) && is_array($context['warnings']) ? array_map('sanitize_text_field', $context['warnings']) : [], ]); } // Technical Data update_post_meta($booking_id, 'daytidy_booking', (string) $draft); update_post_meta($booking_id, 'daytidy_transaction', (string) $transaction); (new PaymentTransactionLedger())->recordAttempt($booking_id, $transaction, array_merge($context, [ 'payment_status' => PaymentStatus::UNPAID, ])); // Publish Booking wp_publish_post($booking_id); /** * Triggers after booking creation */ do_action('DayTidy/A__DayTidy/Booking/Booking__add/added', $booking_id, $context); /** * Auto Confirm */ if (empty($context['suppress_auto_statuses']) && $this->canAutoConfirm($offering, $transaction)) $this->confirm($booking_id, 'auto', $context); return $booking_id; } /** * Mark a Booking as Pending * * @param int|string $id * @param mixed[] $context * @return bool */ public function pending($id, array $context = []) { // Convert Ref to ID $id = $this->refToId($id); // Status if (!$this->transitionSucceeded($this->states->markPending($id, $context))) return false; // Triggers after booking pending do_action('DayTidy/A__DayTidy/Booking/Booking__pending/after', $id, $context); return true; } /** * Confirm a Booking * * @param int|string $id * @param string $mode * @param mixed[] $context * @return bool */ public function confirm($id, string $mode = 'manual', array $context = []) { // Convert Ref to ID $id = $this->refToId($id); $context = $this->modeContext($mode, $context); // Status if (!$this->transitionSucceeded($this->states->confirm($id, $context))) return false; // Triggers after booking confirmation do_action('DayTidy/A__DayTidy/Booking/Booking__confirm/after', $id, $mode, $context); return true; } /** * Reject a Booking * * @param int|string $id * @param mixed[] $context * @return bool */ public function decline($id, array $context = []) { // Convert Ref to ID $id = $this->refToId($id); // Status if (!$this->transitionSucceeded($this->states->decline($id, $context))) return false; // Triggers after booking decline do_action('DayTidy/A__DayTidy/Booking/Booking__decline/after', $id, $context); return true; } /** * Cancel a Booking * * @param int|string $id * @param mixed[] $context * @return bool */ public function cancel($id, array $context = []) { // Convert Ref to ID $id = $this->refToId($id); $cancellation = new Cancellation($this->states); if (!$cancellation->cancel($id, $context)) { $this->last_error = $cancellation->lastError(); return false; } $context = $cancellation->lastContext(); // Triggers after booking cancel do_action('DayTidy/A__DayTidy/Booking/Booking__cancel/after', $id, $context); $this->last_error = ''; return true; } /** * Mark a Booking as Unpaid * * @param int|string $id * @param mixed[] $context * @return bool */ public function unpaid($id, array $context = []) { // Convert Ref to ID $id = $this->refToId($id); // Status if (!$this->transitionSucceeded($this->states->markUnpaid($id, $context))) return false; // Triggers after booking unpaid do_action('DayTidy/A__DayTidy/Booking/Booking__unpaid/after', $id, $context); return true; } /** * Mark a Booking payment as Pending * * @param int|string $id * @param mixed[] $context * @return bool */ public function paymentPending($id, array $context = []) { $id = $this->refToId($id); if (!$this->transitionSucceeded($this->states->markPaymentPending($id, $context))) return false; do_action('DayTidy/A__DayTidy/Booking/Booking__payment_pending/after', $id, $context); return true; } /** * Mark a Booking as Paid * * @param int|string $id * @param mixed[] $context * @return bool */ public function paid($id, array $context = []) { // Convert Ref to ID $id = $this->refToId($id); // Status if (!$this->transitionSucceeded($this->states->markPaid($id, $context))) return false; // Triggers after booking paid do_action('DayTidy/A__DayTidy/Booking/Booking__paid/after', $id, $context); return true; } /** * Mark a Booking payment as Failed * * @param int|string $id * @param mixed[] $context * @return bool */ public function paymentFailed($id, array $context = []) { $id = $this->refToId($id); if (!$this->transitionSucceeded($this->states->markPaymentFailed($id, $context))) return false; do_action('DayTidy/A__DayTidy/Booking/Booking__payment_failed/after', $id, $context); return true; } /** * Mark a Booking payment as Not Required * * @param int|string $id * @param mixed[] $context * @return bool */ public function paymentNotRequired($id, array $context = []) { $id = $this->refToId($id); if (!$this->transitionSucceeded($this->states->markPaymentNotRequired($id, $context))) return false; do_action('DayTidy/A__DayTidy/Booking/Booking__payment_not_required/after', $id, $context); return true; } /** * Mark a Booking payment as Partially Refunded * * @param int|string $id * @param mixed[] $context * @return bool */ public function partiallyRefunded($id, array $context = []) { $id = $this->refToId($id); if (!$this->transitionSucceeded($this->states->markPartiallyRefunded($id, $context))) return false; do_action('DayTidy/A__DayTidy/Booking/Booking__partially_refunded/after', $id, $context); return true; } /** * Mark a Booking as Completed * * @param int|string $id * @param mixed[] $context * @return bool */ public function complete($id, array $context = []) { $id = $this->refToId($id); if (!$this->transitionSucceeded($this->states->complete($id, $context))) return false; do_action('DayTidy/A__DayTidy/Booking/Booking__complete/after', $id, $context); return true; } /** * Mark a Booking as Failed * * @param int|string $id * @param mixed[] $context * @return bool */ public function fail($id, array $context = []) { $id = $this->refToId($id); if (!$this->transitionSucceeded($this->states->fail($id, $context))) return false; (new Records())->fail($id); do_action('DayTidy/A__DayTidy/Booking/Booking__fail/after', $id, $context); return true; } /** * Mark a Booking as Expired * * @param int|string $id * @param mixed[] $context * @return bool */ public function expire($id, array $context = []) { $id = $this->refToId($id); if (!$this->transitionSucceeded($this->states->expire($id, $context))) return false; (new Records())->expire($id); do_action('DayTidy/A__DayTidy/Booking/Booking__expire/after', $id, $context); return true; } /** * Mark a Booking as Active * * @param int|string $id * @param mixed[] $context * @return bool */ public function activate($id, array $context = []) { $id = $this->refToId($id); if (!$this->transitionSucceeded($this->states->markPending($id, $context))) return false; (new Records())->activate($id); do_action('DayTidy/A__DayTidy/Booking/Booking__activate/after', $id, $context); return true; } /** * Mark a Booking as Refunded * * @param int|string $id * @param mixed[] $args * @return mixed[] */ public function refund($id, array $args = []): array { // Convert Ref to ID $id = $this->refToId($id); return (new Refund())->perform($id, $args); } /** * Trash a Booking * * @param int|string $id * @return void */ public function trash($id) { // Convert Ref to ID $id = $this->refToId($id); // Trash Post wp_trash_post($id); // Triggers after booking cancel do_action('DayTidy/A__DayTidy/Booking/Booking__trash/after', $id); } /** * Convert Ref ID to Booking ID * * @param int|string $ref * @return int */ public function refToId($ref): int { // It's already an ID if (is_numeric($ref)) return $ref; // Return ID By Ref return Utils::getIdByMeta('daytidy_ref', $ref); } /** * Check whether a booking can automatically mark as confirmed * * @param Offering $offering * @param Transaction $transaction * @return bool */ public function canAutoConfirm(Offering $offering, Transaction $transaction): bool { // Total Amount $total = $transaction->getTotal(); // Paid if ($total > 0) { $auto_confirm = $offering->getBookingAutoConfirm($transaction->getGateway()); return in_array($auto_confirm, ['paid', 'all', 'on']); } // Free $auto_confirm = $offering->getBookingAutoConfirm(); return in_array($auto_confirm, ['free', 'all']); } private function transitionSucceeded(bool $success): bool { $this->last_error = $success ? '' : $this->states->lastError(); return $success; } /** * @param mixed[] $context * @return mixed[] */ private function modeContext(string $mode, array $context): array { if (!isset($context['source'])) $context['source'] = $mode; return $context; } }