*/ private const TEMPLATE_LABELS = [ 'booking.confirmed.customer' => 'Booking confirmation', 'booking.reminder.customer' => 'Appointment reminder', 'booking.reminder.agent' => 'Staff appointment reminder', 'booking.reminder_soon.customer' => 'On-the-way reminder', 'booking.followup.customer' => 'Thank-you message', 'booking.rebook.customer' => 'Invitation to rebook', 'booking.noshow.customer' => 'No-show follow-up', 'booking.prep.customer' => 'What to expect before visit', 'booking.cancelled_rebook.customer' => 'Rebook after cancellation', 'payment.balance_due.customer' => 'Balance due reminder', 'payment.failed.customer' => 'Payment failed notice', 'customer.welcome' => 'Welcome message', 'intent.resume.customer' => 'Resume booking reminder', 'intent.resume_again.customer' => 'Second resume booking reminder', ]; /** * Metadata merged into a persisted step row. * * @param ActionStep $step Step. * @param ActionResult $result Result. * @param AutomationAction|null $action Registry action. * @return array */ public static function meta( ActionStep $step, ActionResult $result, ?AutomationAction $action ): array { return [ 'action_type' => $step->type(), 'action_label' => null !== $action ? $action->label() : self::type_label( $step->type() ), 'summary' => self::summary( $step, $result ), 'status_label' => self::status_label( $result->status() ), ]; } /** * Operator-facing status label. * * @param string $status Step status. * @return string */ public static function status_label( string $status ): string { return match ( $status ) { 'drafted' => 'Drafted — not sent', 'completed' => 'Done', 'skipped' => 'Skipped', 'failed' => 'Failed', default => ucfirst( $status ), }; } /** * Plain-language step summary. * * @param ActionStep $step Step. * @param ActionResult $result Result. * @return string */ public static function summary( ActionStep $step, ActionResult $result ): string { $rendered = $result->rendered(); if ( is_string( $rendered ) && str_starts_with( $rendered, 'Wait skipped by operator.' ) ) { return 'Wait skipped by operator'; } return match ( $step->type() ) { 'send_notification_template', 'send_email' => self::send_summary( $step ), 'add_customer_tag' => self::tag_summary( $step ), 'add_internal_note' => self::note_summary( $step ), 'change_booking_status' => self::status_change_summary( $step ), 'wait' => self::wait_summary( $step ), 'log_activity' => self::activity_summary( $step ), 'if' => self::if_summary( $rendered ), 'http_integration' => self::http_summary( $rendered ), default => self::fallback_summary( $step, $rendered ), }; } /** * Metadata for a synthetic step row (e.g. skip-wait). * * @param string $type Action type. * @param string $summary Summary text. * @param string $status Step status. * @return array */ public static function meta_for_type( string $type, string $summary, string $status ): array { return [ 'action_type' => $type, 'action_label' => self::type_label( $type ), 'summary' => $summary, 'status_label' => self::status_label( $status ), ]; } /** * Send-notification step summary. * * @param ActionStep $step Step. * @return string */ private static function send_summary( ActionStep $step ): string { $config = $step->config(); $key = isset( $config['template_key'] ) ? (string) $config['template_key'] : ''; $template = self::template_label( $key ); $recipient = self::recipient_label( $step->recipient() ?? AutomationRecipient::Customer ); $intent = SendIntent::Marketing === ( $step->intent() ?? SendIntent::Transactional ) ? 'marketing' : 'transactional'; return sprintf( 'Send “%s” to %s (%s)', $template, $recipient, $intent ); } /** * Customer tag step summary. * * @param ActionStep $step Step. * @return string */ private static function tag_summary( ActionStep $step ): string { $tag = isset( $step->config()['tag'] ) ? trim( (string) $step->config()['tag'] ) : ''; if ( '' === $tag ) { return 'Add a customer tag'; } return sprintf( 'Tag customer as “%s”', $tag ); } /** * Internal note step summary. * * @param ActionStep $step Step. * @return string */ private static function note_summary( ActionStep $step ): string { $note = isset( $step->config()['note'] ) ? trim( (string) $step->config()['note'] ) : ''; if ( '' === $note ) { return 'Add an internal note'; } return $note; } /** * Booking status change step summary. * * @param ActionStep $step Step. * @return string */ private static function status_change_summary( ActionStep $step ): string { $status = isset( $step->config()['status'] ) ? (string) $step->config()['status'] : ''; $label = self::humanise_token( $status ); return '' === $label ? 'Change booking status' : sprintf( 'Change booking status to %s', $label ); } /** * Wait step summary. * * @param ActionStep $step Step. * @return string */ private static function wait_summary( ActionStep $step ): string { $minutes = $step->delay_minutes(); if ( $minutes > 0 ) { return sprintf( 'Wait %d minute%s before continuing', $minutes, 1 === $minutes ? '' : 's' ); } return 'Pause before the next step'; } /** * Activity log step summary. * * @param ActionStep $step Step. * @return string */ private static function activity_summary( ActionStep $step ): string { $config = $step->config(); foreach ( [ 'message', 'note', 'n' ] as $key ) { if ( isset( $config[ $key ] ) && is_scalar( $config[ $key ] ) && '' !== trim( (string) $config[ $key ] ) ) { return sprintf( 'Log activity: %s', trim( (string) $config[ $key ] ) ); } } return 'Log activity'; } /** * Condition branch step summary. * * @param string|null $rendered Raw result text. * @return string */ private static function if_summary( ?string $rendered ): string { if ( 'if:pass' === $rendered ) { return 'Condition passed — continue on “yes” branch'; } if ( 'if:fail' === $rendered ) { return 'Condition failed — continue on “no” branch'; } return 'Check condition'; } /** * Webhook step summary. * * @param string|null $rendered Raw result text. * @return string */ private static function http_summary( ?string $rendered ): string { if ( ! is_string( $rendered ) || '' === $rendered ) { return 'Call webhook'; } $line = strtok( $rendered, "\n" ); return is_string( $line ) && '' !== $line ? 'Call webhook: ' . $line : 'Call webhook'; } /** * Fallback when no specialised formatter applies. * * @param ActionStep $step Step. * @param string|null $rendered Raw result text. * @return string */ private static function fallback_summary( ActionStep $step, ?string $rendered ): string { if ( is_string( $rendered ) && '' !== $rendered && ! str_contains( $rendered, '{' ) ) { return $rendered; } return self::type_label( $step->type() ); } /** * Resolve a template key to a label. * * @param string $key Template key. * @return string */ private static function template_label( string $key ): string { if ( isset( self::TEMPLATE_LABELS[ $key ] ) ) { return self::TEMPLATE_LABELS[ $key ]; } if ( '' === $key ) { return 'Notification'; } $trimmed = preg_replace( '/^(booking|customer|payment|intent)\./', '', $key ); $trimmed = is_string( $trimmed ) ? preg_replace( '/\.(customer|agent)$/', '', $trimmed ) : $key; $trimmed = is_string( $trimmed ) ? $trimmed : $key; return self::humanise_token( $trimmed ); } /** * Recipient label for summaries. * * @param AutomationRecipient $recipient Recipient. * @return string */ private static function recipient_label( AutomationRecipient $recipient ): string { return match ( $recipient ) { AutomationRecipient::Customer => 'customer', AutomationRecipient::Agent => 'staff member', AutomationRecipient::Admin => 'administrator', }; } /** * Action type label. * * @param string $type Action type. * @return string */ private static function type_label( string $type ): string { return match ( $type ) { 'send_notification_template' => 'Send notification', 'send_email' => 'Send email', 'add_customer_tag' => 'Add customer tag', 'add_internal_note' => 'Add internal note', 'change_booking_status' => 'Change booking status', 'wait' => 'Wait', 'log_activity' => 'Log activity', 'http_integration' => 'Webhook', 'if' => 'Condition', 'goto' => 'Go to step', default => self::humanise_token( $type ), }; } /** * Turn a machine token into title case words. * * @param string $token Token. * @return string */ private static function humanise_token( string $token ): string { $token = str_replace( [ '_', '.', '-' ], ' ', $token ); $token = preg_replace( '/\s+/', ' ', $token ); $token = is_string( $token ) ? trim( $token ) : ''; if ( '' === $token ) { return ''; } return ucfirst( $token ); } }