booking_repo = $booking_repo ?: new BookingRepository();
add_action( 'cleannetic_booking_created', [ $this, 'on_booking_created' ], 10, 2 );
add_action( 'cleannetic_booking_status_changed', [ $this, 'on_status_changed' ], 10, 3 );
}
/**
* Lets callers (e.g. the demo importer) mute booking emails.
*/
private function emails_enabled(): bool {
return (bool) apply_filters( 'cleannetic_send_booking_emails', true );
}
public function on_booking_created( int $booking_id, ?array $booking = null ): void {
if ( ! $this->emails_enabled() ) {
return;
}
$booking = $booking ?: $this->booking_repo->find( $booking_id );
if ( ! $booking ) {
return;
}
if ( Settings::get( 'send_customer_email', true ) && ! empty( $booking['customer_email'] ) ) {
$this->send_customer_confirmation( $booking );
}
if ( Settings::get( 'send_admin_email', true ) ) {
$this->send_admin_alert( $booking );
}
}
public function on_status_changed( int $booking_id, string $new_status, string $old_status ): void {
if ( ! $this->emails_enabled() || ! Settings::get( 'send_status_emails', true ) ) {
return;
}
$booking = $this->booking_repo->find( $booking_id );
if ( ! $booking || empty( $booking['customer_email'] ) ) {
return;
}
$business_name = Settings::get( 'business_name', get_bloginfo( 'name' ) );
$status_label = BookingStatus::label( $new_status );
$subject = sprintf(
/* translators: 1: status, 2: booking ref, 3: business name */
__( 'Booking %1$s — %2$s | %3$s', 'cleannetic' ),
$status_label,
$booking['booking_ref'],
$business_name
);
$body = $this->render_email_layout(
/* translators: %s: booking status */
sprintf( __( 'Booking Status Updated: %s', 'cleannetic' ), $status_label ),
sprintf(
/* translators: 1: customer name, 2: booking ref, 3: service name, 4: booking status */
__( 'Hello %1$s, your booking %2$s for %3$s has been marked as %4$s.', 'cleannetic' ),
esc_html( $booking['customer_name'] ),
esc_html( $booking['booking_ref'] ),
esc_html( $booking['service_name'] ),
esc_html( $status_label )
),
$booking
);
$this->send( $booking['customer_email'], $subject, $body );
}
/**
* Replaces the merge tags offered in Settings → Email Notifications.
*
* @param bool $for_html Escape values when the result is rendered as HTML.
*/
private function replace_merge_tags( string $template, array $booking, bool $for_html = false ): string {
$address = implode( ', ', array_filter( [ $booking['address_data']['street'] ?? '', $booking['address_data']['city'] ?? '', $booking['address_data']['zip'] ?? '' ] ) );
$values = [
'{customer_name}' => (string) $booking['customer_name'],
'{service_name}' => (string) $booking['service_name'],
'{booking_ref}' => (string) $booking['booking_ref'],
'{start_time}' => Formatter::datetime( $booking['start_datetime'] ),
'{total_price}' => Formatter::price( (float) $booking['total_amount'] ),
'{customer_address}' => $address,
'{business_name}' => (string) Settings::get( 'business_name', get_bloginfo( 'name' ) ),
];
if ( $for_html ) {
$values = array_map( 'esc_html', $values );
}
return strtr( $template, $values );
}
private function is_quote( array $booking ): bool {
return BookingStatus::QUOTE_REQUESTED === $booking['status'];
}
public function send_customer_confirmation( array $booking ): void {
if ( $this->is_quote( $booking ) ) {
/* translators: %s: booking ref */
$subject = sprintf( __( 'We received your quote request — %s', 'cleannetic' ), $booking['booking_ref'] );
$heading = __( 'Quote request received', 'cleannetic' );
$intro = $this->replace_merge_tags( __( 'Thanks for contacting {business_name}. We will review your request and get back to you shortly with a final price.', 'cleannetic' ), $booking, true );
$this->send( $booking['customer_email'], $subject, $this->render_email_layout( $heading, $intro, $booking ) );
return;
}
$subject_tpl = (string) Settings::get( 'customer_email_subject', __( 'Your cleaning is booked — {booking_ref}', 'cleannetic' ) );
$subject = $this->replace_merge_tags( $subject_tpl, $booking );
$heading = $this->replace_merge_tags( (string) Settings::get( 'customer_email_heading', __( 'You are all set!', 'cleannetic' ) ), $booking );
$intro = (string) Settings::get( 'customer_email_intro', __( 'Thanks for booking with us. Here are your appointment details.', 'cleannetic' ) );
$intro = nl2br( $this->replace_merge_tags( esc_html( $intro ), $booking, true ) );
$body = $this->render_email_layout( $heading, $intro, $booking );
// Generate .ics attachment.
$attachments = [];
$ics_path = $this->generate_ics_file( $booking );
if ( $ics_path && file_exists( $ics_path ) ) {
$attachments[] = $ics_path;
}
$this->send( $booking['customer_email'], $subject, $body, $attachments );
if ( $ics_path && file_exists( $ics_path ) ) {
wp_delete_file( $ics_path );
}
}
public function send_admin_alert( array $booking ): void {
$admin_email = (string) Settings::get( 'admin_notification_email', get_option( 'admin_email' ) );
if ( empty( $admin_email ) || ! is_email( $admin_email ) ) {
return;
}
$subject_tpl = (string) Settings::get( 'admin_email_subject', __( 'New booking {booking_ref} — {service_name}', 'cleannetic' ) );
$subject = $this->replace_merge_tags( $subject_tpl, $booking );
if ( $this->is_quote( $booking ) ) {
/* translators: %s: booking ref */
$heading = sprintf( __( 'New Quote Request (#%s)', 'cleannetic' ), $booking['booking_ref'] );
/* translators: %s: customer name */
$intro = sprintf( __( '%s has requested a quote. Review the details and reply with a price.', 'cleannetic' ), esc_html( $booking['customer_name'] ) );
} else {
/* translators: %s: booking ref */
$heading = sprintf( __( 'New Booking Received (#%s)', 'cleannetic' ), $booking['booking_ref'] );
$intro = 'admin' === ( $booking['source'] ?? '' )
/* translators: %s: customer name */
? sprintf( __( 'A new booking was created in the dashboard for %s.', 'cleannetic' ), esc_html( $booking['customer_name'] ) )
/* translators: %s: customer name */
: sprintf( __( 'A new cleaning booking has been placed online by %s.', 'cleannetic' ), esc_html( $booking['customer_name'] ) );
}
$body = $this->render_email_layout( $heading, $intro, $booking, true );
$this->send( $admin_email, $subject, $body );
}
private function render_email_layout( string $heading, string $intro, array $booking, bool $is_admin = false ): string {
$business_name = (string) Settings::get( 'business_name', get_bloginfo( 'name' ) );
$accent_color = (string) Settings::get( 'accent_color', '#0D9488' );
$formatted_price = Formatter::price( (float) $booking['total_amount'] );
$formatted_date = Formatter::datetime( $booking['start_datetime'] );
$address_lines = [];
if ( ! empty( $booking['address_data']['street'] ) ) {
$address_lines[] = esc_html( $booking['address_data']['street'] );
}
if ( ! empty( $booking['address_data']['city'] ) || ! empty( $booking['address_data']['zip'] ) ) {
$address_lines[] = esc_html( trim( ( $booking['address_data']['city'] ?? '' ) . ' ' . ( $booking['address_data']['zip'] ?? '' ) ) );
}
$address_html = ! empty( $address_lines ) ? implode( '
', $address_lines ) : esc_html__( 'Address on file', 'cleannetic' );
if ( ! empty( $booking['address_data']['gate_code'] ) ) {
/* translators: %s: gate or access code. */
$address_html .= '
' . sprintf( esc_html__( 'Gate / Access Code: %s', 'cleannetic' ), esc_html( $booking['address_data']['gate_code'] ) ) . '';
}
ob_start();
?>