reservations->find( $id ); if ( null === $reservation ) { return; } $this->to_guest( 'confirmed' === $reservation->status ? self::CONFIRMED : self::RECEIVED, $reservation ); if ( 'online' === $reservation->source ) { $this->to_owner( self::NEW_RESERVATION, $reservation ); } } public function on_status_changed( int $id, mixed $reservation, string $old_status = '' ): void { if ( ! $reservation instanceof Reservation || $old_status === $reservation->status ) { return; } if ( 'confirmed' === $reservation->status && 'pending' === $old_status ) { $this->to_guest( self::CONFIRMED, $reservation ); } elseif ( 'cancelled' === $reservation->status ) { $this->to_guest( self::CANCELLED, $reservation ); } } // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter -- the hook passes them. public function on_moved( int $id, mixed $reservation, mixed $before = null ): void { if ( $reservation instanceof Reservation ) { $this->to_guest( self::MOVED, $reservation ); } } public function on_customer_cancelled( mixed $reservation ): void { if ( $reservation instanceof Reservation ) { $this->to_owner( self::CUSTOMER_CANCELLED, $reservation ); } } /** * @param mixed $before The reservation at its old time. */ public function on_customer_moved( mixed $reservation, mixed $before = null ): void { if ( $reservation instanceof Reservation ) { $previous = $before instanceof Reservation ? Emails::when( $before->date, $before->time ) : ''; $this->to_owner( self::CUSTOMER_MOVED, $reservation, [ 'previous_when' => $previous ] ); } } public function send_reminder( Reservation $reservation ): void { $this->to_guest( self::REMINDER, $reservation ); } /** * The guest's self-service link: the reservation page with the id and * its token. '' when there is no page, no email, or nothing left to change. */ public static function manage_link( Reservation $reservation ): string { $base = ReservationPage::url(); if ( '' === $base || '' === $reservation->email || ! in_array( $reservation->status, Reservation::OPEN, true ) ) { return ''; } return add_query_arg( [ 'fxtr_reservation' => $reservation->id, 'fxtr_token' => Token::make( $reservation->id, $reservation->email, 'reservation' ), ], $base ); } /** * Renders one email as the owner built it (EmailTemplates), or null for * a type it does not know or when a `fxtr_email_compose` callback * returned something that is not a mail. An email the owner turned off * still renders here; the senders below skip it. * * @param array $extra * @return array{subject:string,body:string}|null */ public function compose( string $type, Reservation $reservation, array $extra = [] ): ?array { $template = EmailTemplates::get( $type ); if ( null === $template ) { return null; } $values = $extra + self::values( $reservation ); $mail = self::render( $template, $reservation, EmailTemplates::style(), $values ); /** * Lets a site render a reservation email its own way. * * @param array{subject:string,body:string} $mail * @param string $type * @param array $values */ $filtered = apply_filters( 'fxtr_email_compose', $mail, $type, $values ); // A callback may hand back anything, whatever the docblock says. /** @var mixed $filtered */ if ( ! is_array( $filtered ) || ! is_string( $filtered['subject'] ?? null ) || ! is_string( $filtered['body'] ?? null ) ) { return null; } return [ 'subject' => $filtered['subject'], 'body' => $filtered['body'], ]; } /** * One email's subject and body from a template, without the filter: what * compose() sends, and what the builder previews (`$editor`). * * @param Template $template * @param array $style * @param array $values * @return array{subject:string,body:string} */ public static function render( array $template, Reservation $reservation, array $style, array $values, bool $editor = false ): array { return [ 'subject' => wp_specialchars_decode( Emails::fill( $template['subject'], $values, false ), ENT_QUOTES ), 'body' => Emails::html( $template['blocks'], $values, [ 'style' => $style, 'answers' => self::answers( $reservation ), 'editor' => $editor, ] ), ]; } /** * The guest's answers to the owner's own fields that show in emails. * * @return list */ private static function answers( Reservation $reservation ): array { $out = []; foreach ( $reservation->fields as $answer ) { if ( $answer['email'] ) { $out[] = [ $answer['label'], $answer['value'] ]; } } return $out; } /** * @return array */ public static function values( Reservation $reservation ): array { return [ 'reservation_id' => (string) $reservation->id, 'booking_date' => Emails::format_date( $reservation->date ), 'booking_time' => Emails::format_time( $reservation->date, $reservation->time ), 'party_size' => (string) $reservation->party_size, 'customer_name' => $reservation->customer_name, 'customer_phone' => $reservation->phone, 'customer_email' => $reservation->email, 'notes' => $reservation->notes, 'manage_link' => self::manage_link( $reservation ), 'admin_link' => admin_url( 'admin.php?page=flexatech-table-reservations#/bookings/reservations?date=' . $reservation->date ), 'previous_when' => '', 'site_name' => wp_specialchars_decode( (string) get_option( 'blogname', '' ), ENT_QUOTES ), 'site_url' => home_url( '/' ), ]; } private function to_guest( string $type, Reservation $reservation ): void { if ( '' === $reservation->email || ! EmailTemplates::enabled( $type ) ) { return; } $mail = $this->compose( $type, $reservation ); if ( null === $mail ) { return; } $this->mailer->send( $reservation->email, $mail['subject'], $mail['body'], Mailer::ics_file( 'reservation-' . $reservation->id, IcsBuilder::reservation( $reservation ) ), Mailer::owner_email() ); } /** * @param array $extra */ private function to_owner( string $type, Reservation $reservation, array $extra = [] ): void { $owner = Mailer::owner_email(); $mail = '' !== $owner && EmailTemplates::enabled( $type ) ? $this->compose( $type, $reservation, $extra ) : null; if ( null === $mail ) { return; } // A reply from the owner reaches the guest. $this->mailer->send( $owner, $mail['subject'], $mail['body'], [], $reservation->email ); } }