seriesNotify($this->notifications,$series,$event,$selected);} public function send( array $booking, string $event ): void { $id = null; $switched = false; $mail_error = ''; $capture = static function ( \WP_Error $error ) use ( &$mail_error ): void { $mail_error = $error->get_error_message(); }; try { $user = $booking['customer_user_id'] ? get_userdata( $booking['customer_user_id'] ) : false; if ( ! $user ) { return; } // External/unlinked customers have no WordPress email recipient. $requestedLocale = $user ? get_user_meta( $user->ID, 'locale', true ) : ''; $locale = $requestedLocale; $locale = \ABeCe\Booking\Support\LocaleContext::resolve($locale); $siteLocale=get_option('WPLANG')?:(defined('WPLANG')&&WPLANG?WPLANG:'en_US'); $switched = switch_to_locale( $locale ); $resource = $this->resources->find( $booking['resource_id'] ); $start = Time::timestamp( Time::fromSql( $booking['starts_at'] ) ); $end = Time::timestamp( Time::fromSql( $booking['ends_at'] ) ); /* translators: %s: reservation reference. */ $subject = sprintf( 'no_show' === $event ? __( 'Booking marked as no-show: %s', 'abece-booking' ) : ( 'cancelled' === $event ? __( 'Booking cancelled: %s', 'abece-booking' ) : __( 'Booking confirmed: %s', 'abece-booking' ) ), $booking['reference'] ); $body = implode( "\n", [ /* translators: %s: reservation reference. */ sprintf( __( 'Reference: %s', 'abece-booking' ), $booking['reference'] ), /* translators: %s: resource name. */ sprintf( __( 'Resource: %s', 'abece-booking' ), $resource['name'] ?? '' ), /* translators: %s: date formatted using the site settings. */ sprintf( __( 'Date: %s', 'abece-booking' ), wp_date(get_option('date_format'),$start->getTimestamp()) ), /* translators: %s: local start and end times. */ sprintf( __( 'Time: %s', 'abece-booking' ), wp_date(get_option('time_format'),$start->getTimestamp()) . ' – ' . wp_date(get_option('time_format'),$end->getTimestamp()) ), /* translators: %d: reservation duration in minutes. */ sprintf( __( 'Duration: %d minutes', 'abece-booking' ), ( $end->getTimestamp() - $start->getTimestamp() ) / 60 ), /* translators: %d: total participant count. */ sprintf( __( 'Total players: %d', 'abece-booking' ), $booking['participant_count'] ), /* translators: %s: participant count. */ sprintf( __( 'Participants: %s', 'abece-booking' ), (string)$booking['participant_count'] ), /* translators: %s: translated reservation status. */ sprintf( __( 'Status: %s', 'abece-booking' ), 'no_show' === $event ? __( 'No-show', 'abece-booking' ) : ( 'cancelled' === $event ? __( 'Cancelled', 'abece-booking' ) : __( 'Confirmed', 'abece-booking' ) ) ), ] ); /* translators: %s: localized monetary amount including its currency. */ $body .= "\n" . sprintf( __( 'Base price: %s', 'abece-booking' ), \ABeCe\Booking\Support\LocalizedFormat::money( $booking['base_amount'], $booking['currency'] ) ); /* translators: %s: localized monetary amount including its currency. */ if ( $booking['discount_amount'] > 0 ) { $body .= "\n" . sprintf( __( 'Member benefit: -%s', 'abece-booking' ), \ABeCe\Booking\Support\LocalizedFormat::money( $booking['discount_amount'], $booking['currency'] ) ); } /* translators: %s: localized monetary amount including its currency. */ if ( 'confirmed' === $event ) $body .= "\n" . sprintf( __( 'Amount due: %s', 'abece-booking' ), \ABeCe\Booking\Support\LocalizedFormat::money( $booking['final_amount'], $booking['currency'] ) ); $method = match ( $booking['payment_method'] ) { 'bank_transfer'=>__('Bank transfer','abece-booking'), 'qr_payment'=>__('QR payment','abece-booking'), 'credit' => __( 'Credit', 'abece-booking' ), 'pay_on_site' => __( 'Pay on site', 'abece-booking' ), 'cash' => __( 'Cash', 'abece-booking' ), 'manual' => __( 'Manual payment', 'abece-booking' ), default => __( 'Not required', 'abece-booking' ) }; $status = match ( $booking['payment_status'] ) { 'refund_pending' => __( 'Refund pending', 'abece-booking' ), 'partially_refunded' => __( 'Partially refunded', 'abece-booking' ), 'partially_paid' => __( 'Partially paid', 'abece-booking' ), 'paid' => __( 'Paid', 'abece-booking' ), 'refunded' => __( 'Refunded', 'abece-booking' ), 'not_required' => __( 'Not required', 'abece-booking' ), default => __( 'Unpaid', 'abece-booking' ) }; /* translators: First %s: payment method; second %s: payment status. */ $body .= "\n" . sprintf( __( 'Payment: %s', 'abece-booking' ), $method ) . "\n" . sprintf( __( 'Payment status: %s', 'abece-booking' ), $status ); if ( in_array( $event, [ 'cancelled', 'no_show' ], true ) ) { $state = $this->settlement->calculate( $booking ); /* translators: %s: localized monetary amount including its currency. */ $body .= "\n" . sprintf( __( 'Original booking price: %s', 'abece-booking' ), \ABeCe\Booking\Support\LocalizedFormat::money( $booking['final_amount'], $booking['currency'] ) ); /* translators: %s: localized monetary amount including its currency. */ $body .= "\n" . sprintf( 'cancelled' === $event ? __( 'Cancellation fee: %s', 'abece-booking' ) : __( 'No-show fee: %s', 'abece-booking' ), \ABeCe\Booking\Support\LocalizedFormat::money( 'cancelled' === $event ? $booking['cancellation_fee'] : $booking['no_show_fee'], $booking['currency'] ) ); /* translators: %s: localized monetary amount including its currency. */ if ( 'credit' === $booking['payment_method'] ) $body .= "\n" . sprintf( __( 'Returned to credit: %s', 'abece-booking' ), \ABeCe\Booking\Support\LocalizedFormat::money( $state['refunded_amount'], $booking['currency'] ) ); /* translators: %s: localized monetary amount including its currency. */ $body .= "\n" . sprintf( __( 'Refund due: %s', 'abece-booking' ), \ABeCe\Booking\Support\LocalizedFormat::money( $state['refund_due'], $booking['currency'] ) ); /* translators: %s: localized monetary amount including its currency. */ $body .= "\n" . sprintf( __( 'Outstanding: %s', 'abece-booking' ), \ABeCe\Booking\Support\LocalizedFormat::money( $state['outstanding_amount'], $booking['currency'] ) ); } $instructions=\ABeCe\Booking\Edition\Modules::current()->instructions(new PaymentSettingsService(new \ABeCe\Booking\Repository\PaymentSettingsRepository()),$this->bookings,$this->settlement)->get($booking); if($instructions){$bank=$instructions['bank_account'];$body.="\n".__('Payment instructions','abece-booking');foreach([__('Beneficiary','abece-booking')=>$bank['recipient_name'],__('IBAN','abece-booking')=>$bank['iban'],__('BIC/SWIFT','abece-booking')=>$bank['bic'],__('Payment reference','abece-booking')=>$instructions['reference'],__('Amount due','abece-booking')=>\ABeCe\Booking\Support\LocalizedFormat::money($instructions['amount'],$instructions['currency']),__('Variable symbol','abece-booking')=>$instructions['variable_symbol'],__('Message for the recipient','abece-booking')=>$instructions['message'],__('Bank account','abece-booking')=>$bank['local_account_display']??($bank['account_number']?$bank['account_number'].($bank['bank_code']?'/'.$bank['bank_code']:''):null),__('Additional instructions','abece-booking')=>$bank['additional_instructions']??null] as $label=>$value)if($value!==null&&$value!=='')$body.="\n".$label.': '.$value;} $url=FrontendIntegrationService::bookingUrl($booking);if($url)$body.="\n".__('Booking details','abece-booking').': '.$url; if($event==='confirmed'){$rules=new BookingRulesService(new \ABeCe\Booking\Repository\SettingsRepository());$intro=$rules->emailText('confirmation_email_intro',$requestedLocale,$siteLocale);$outro=$rules->emailText('confirmation_email_outro',$requestedLocale,$siteLocale);$body=($intro!==''?$intro."\n\n":'').$body.($outro!==''?"\n\n".$outro:'');} $now = current_time( 'mysql', true ); $id = $this->notifications->insert( [ 'booking_id' => $booking['id'], 'user_id' => $booking['customer_user_id'], 'notification_type' => 'booking_' . $event, 'channel' => 'email', 'recipient' => $user ? $user->user_email : '', 'subject' => $subject, 'status' => 'pending', 'attempts' => 0, 'created_at' => $now, 'updated_at' => $now ] ); add_action( 'wp_mail_failed', $capture ); $sent = $user && is_email( $user->user_email ) && wp_mail( $user->user_email, $subject, $body, [ 'Content-Type: text/plain; charset=UTF-8' ] ); $this->notifications->update( $id, [ 'status' => $sent ? 'sent' : 'failed', 'attempts' => 1, 'sent_at' => $sent ? current_time( 'mysql', true ) : null, 'last_error' => $sent ? null : __( 'Email delivery failed.', 'abece-booking' ), 'updated_at' => current_time( 'mysql', true ) ] ); } catch ( Throwable $error ) { if ( null !== $id ) { try { $this->notifications->update( $id, [ 'status' => 'failed', 'attempts' => 1, 'last_error' => __( 'Email delivery failed.', 'abece-booking' ), 'updated_at' => current_time( 'mysql', true ) ] ); } catch ( Throwable ) { /* Booking is already committed. */ } } // Avoid personal data in logs. Operators can inspect persisted delivery metadata. error_log( 'ABeCe Booking: notification processing failed; inspect delivery status in booking administration.' ); } finally { remove_action( 'wp_mail_failed', $capture ); if ( $switched ) { restore_previous_locale(); } } } }