activity_types = include plugin_dir_path( ATKAI_PLUGIN_FILE ) . '/activities/types.php';
$this->logger = $logger;
if ( did_action( 'plugins_loaded' ) ) {
$this->maybe_register_hooks();
} else {
add_action( 'plugins_loaded', array( $this, 'maybe_register_hooks' ), 99 );
}
if ( did_action( 'woocommerce_loaded' ) || did_action( 'woocommerce_init' ) ) {
$this->maybe_register_hooks();
} else {
add_action( 'woocommerce_loaded', array( $this, 'maybe_register_hooks' ), 99 );
add_action( 'woocommerce_init', array( $this, 'maybe_register_hooks' ), 99 );
}
}
/**
* Attach WooCommerce hooks once the dependency is available.
*/
public function maybe_register_hooks() {
if ( $this->hooks_registered || ! $this->is_woocommerce_ready() ) {
return;
}
$this->hooks_registered = true;
add_action( 'woocommerce_new_order', array( $this, 'log_new_order' ), 10, 2 );
add_action( 'woocommerce_order_edit_status', array( $this, 'queue_manual_status_change' ), 10, 2 );
add_action( 'woocommerce_order_status_changed', array( $this, 'log_status_change' ), 10, 4 );
add_action( 'woocommerce_process_shop_order_meta', array( $this, 'queue_admin_order_status_change' ), 99, 2 );
add_action( 'woocommerce_order_refunded', array( $this, 'log_refund_created' ), 10, 2 );
add_action( 'woocommerce_trash_order', array( $this, 'log_trashed_order' ), 10, 1 );
add_action( 'woocommerce_before_delete_order', array( $this, 'log_deleted_order' ), 10, 1 );
add_action( 'shutdown', array( $this, 'flush_manual_status_changes' ), 20 );
}
/**
* Verify WooCommerce is loaded and usable.
*
* @return bool
*/
private function is_woocommerce_ready() {
return class_exists( 'WooCommerce' ) && function_exists( 'wc_get_order' );
}
/**
* Log every new order.
*
* @param int $order_id Order ID.
* @param WC_Order|false $order Order instance when provided by the hook.
*/
public function log_new_order( $order_id, $order = false ) {
$order = $this->ensure_order( $order_id, $order );
if ( ! $order ) {
return;
}
// Skip draft orders: admin "Add order" auto-drafts and checkout-block
// drafts fire this hook before the order is actually finalized.
if ( in_array( $order->get_status(), array( 'auto-draft', 'checkout-draft' ), true ) ) {
return;
}
$order_number = $order->get_order_number();
$total = $this->format_price( $order->get_total(), $order );
$description = sprintf(
/* translators: 1: Order number, 2: formatted order total */
'WooCommerce: New order #%1$s created for %2$s.',
esc_html( $order_number ),
$total
);
$this->log_event(
$this->resolve_user_id(),
'new_order',
$description,
'new_order',
$order
);
}
/**
* Log order status transitions.
*
* @param int $order_id Order ID.
* @param string $old_status Old order status.
* @param string $new_status New order status.
* @param WC_Order|false $order Order instance when provided by the hook.
*/
public function log_status_change( $order_id, $old_status, $new_status, $order = false ) {
if ( $old_status === $new_status ) {
return;
}
// Refunds are logged via woocommerce_order_refunded; skip the
// accompanying status change to avoid a duplicate entry.
if ( 'refunded' === $new_status ) {
return;
}
$order = $this->ensure_order( $order_id, $order );
if ( ! $order ) {
return;
}
$status_key = $this->normalize_status_key( $new_status );
if ( empty( $status_key ) || empty( $this->activity_types['woocommerce'][ $status_key ] ) ) {
return;
}
$order_number = $order->get_order_number();
$description = sprintf(
/* translators: 1: Order number, 2: previous status, 3: new status */
'WooCommerce: Order #%1$s status changed from %2$s to %3$s.',
esc_html( $order_number ),
$this->format_status_label( $old_status ),
$this->format_status_label( $new_status )
);
$this->log_event(
$this->resolve_user_id(),
$status_key,
$description,
$status_key,
$order
);
$this->logged_status_updates[ $this->get_status_update_key( $order_id, $status_key ) ] = true;
}
/**
* Queue manual admin status updates as a fallback for order edit screens.
*
* @param int $order_id Order ID.
* @param string $new_status New order status.
*/
public function queue_manual_status_change( $order_id, $new_status ) {
$status_key = $this->normalize_status_key( $new_status );
if ( empty( $status_key ) || empty( $this->activity_types['woocommerce'][ $status_key ] ) ) {
return;
}
$this->manual_status_updates[ (int) $order_id ] = $status_key;
}
/**
* Queue status changes from the admin order edit save request.
*
* @param int $order_id Order ID.
* @param WC_Order|WP_Post $order Order object or post object.
*/
public function queue_admin_order_status_change( $order_id, $order = null ) {
if ( empty( $_POST['order_status'] ) ) {
return;
}
$new_status = sanitize_text_field( wp_unslash( $_POST['order_status'] ) );
$old_status = isset( $_POST['post_status'] )
? sanitize_text_field( wp_unslash( $_POST['post_status'] ) )
: '';
if ( $old_status && $this->normalize_status_key( $old_status ) === $this->normalize_status_key( $new_status ) ) {
return;
}
$this->queue_manual_status_change( $order_id, $new_status );
}
/**
* Log queued manual status changes if the standard status hook did not.
*/
public function flush_manual_status_changes() {
if ( empty( $this->manual_status_updates ) ) {
return;
}
foreach ( $this->manual_status_updates as $order_id => $status_key ) {
if ( ! empty( $this->logged_status_updates[ $this->get_status_update_key( $order_id, $status_key ) ] ) ) {
continue;
}
$order = $this->ensure_order( $order_id, false );
if ( ! $order ) {
continue;
}
$description = sprintf(
/* translators: 1: Order number, 2: new status */
'WooCommerce: Order #%1$s status changed to %2$s.',
esc_html( $order->get_order_number() ),
$this->format_status_label( str_replace( '_', '-', str_replace( 'status_', '', $status_key ) ) )
);
$this->log_event(
$this->resolve_user_id(),
$status_key,
$description,
$status_key,
$order
);
}
}
/**
* Log newly created refunds (covers partial refunds).
*
* @param int $order_id Order ID.
* @param int $refund_id Refund ID.
*/
public function log_refund_created( $order_id, $refund_id ) {
$order = wc_get_order( $order_id );
$refund = wc_get_order( $refund_id );
if ( ! $order || ! $refund ) {
return;
}
$amount = $this->format_price( $refund->get_amount(), $order );
$reason = $refund->get_reason();
$order_label = esc_html( $order->get_order_number() );
$description = sprintf(
/* translators: 1: Refund amount, 2: Order number, 3: Refund reason */
'WooCommerce: Refund of %1$s issued for order #%2$s%3$s.',
$amount,
$order_label,
$reason ? ' (' . esc_html( $reason ) . ')' : ''
);
$this->log_event(
$this->resolve_user_id(),
'refund',
$description,
'refund',
$order
);
}
/**
* Log when an order is moved to the trash.
*
* @param int $order_id Order ID.
*/
public function log_trashed_order( $order_id ) {
$order = $this->ensure_order( $order_id, false );
if ( ! $order ) {
return;
}
$description = sprintf(
/* translators: %s: Order number */
'WooCommerce: Order #%s moved to trash.',
esc_html( $order->get_order_number() )
);
$this->log_event(
$this->resolve_user_id(),
'order_trashed',
$description,
'order_trashed',
$order
);
}
/**
* Log when an order is permanently deleted.
*
* Hooked before deletion so the order data is still readable.
*
* @param int $order_id Order ID.
*/
public function log_deleted_order( $order_id ) {
$order = $this->ensure_order( $order_id, false );
if ( ! $order ) {
return;
}
$description = sprintf(
/* translators: %s: Order number */
'WooCommerce: Order #%s permanently deleted.',
esc_html( $order->get_order_number() )
);
$this->log_event(
$this->resolve_user_id(),
'order_deleted',
$description,
'order_deleted',
$order
);
}
/**
* Make sure we work with a WC_Order instance.
*
* @param int $order_id Order ID.
* @param WC_Order|false $order Order instance when provided.
*
* @return WC_Order|false
*/
private function ensure_order( $order_id, $order ) {
if ( $order instanceof WC_Order ) {
return $order;
}
return wc_get_order( $order_id );
}
/**
* Resolve the user responsible for the entry.
*
* Returns 0 (system) when no user is in context, e.g. payment gateway
* webhooks or cron-driven status changes, so those are not misattributed
* to the order's customer.
*
* @return int
*/
private function resolve_user_id() {
return (int) get_current_user_id();
}
/**
* Normalize WooCommerce status to internal key.
*
* @param string $status WooCommerce status (without wc- prefix).
*
* @return string
*/
private function normalize_status_key( $status ) {
$status = sanitize_key( $status );
if ( empty( $status ) ) {
return '';
}
if ( strpos( $status, 'wc-' ) === 0 ) {
$status = substr( $status, 3 );
}
$status = str_replace( '-', '_', $status );
return 'status_' . $status;
}
/**
* Build a stable key for one order/status update.
*
* @param int $order_id Order ID.
* @param string $status_key Internal status key.
*
* @return string
*/
private function get_status_update_key( $order_id, $status_key ) {
return absint( $order_id ) . '|' . $status_key;
}
/**
* Format status labels for log descriptions.
*
* @param string $status WooCommerce status (without wc- prefix).
*
* @return string
*/
private function format_status_label( $status ) {
$prefixed_status = strpos( $status, 'wc-' ) === 0 ? $status : 'wc-' . $status;
if ( function_exists( 'wc_get_order_status_name' ) ) {
$label = wc_get_order_status_name( $prefixed_status );
if ( $label ) {
return esc_html( $label );
}
}
return esc_html( ucwords( str_replace( '-', ' ', $status ) ) );
}
/**
* Format prices while staying safe when Woo helpers are unavailable.
*
* @param float $amount Amount.
* @param WC_Order $order Order instance.
*
* @return string
*/
private function format_price( $amount, $order ) {
if ( function_exists( 'wc_price' ) ) {
return wc_price( $amount, array( 'currency' => $order->get_currency() ) );
}
return sprintf( '%0.2f %s', $amount, $order->get_currency() );
}
/**
* Centralized logger call.
*
* @param int $user_id User ID responsible for the log.
* @param string $action_key Activity type key.
* @param string $description Activity description.
* @param string $status Activity status slug.
* @param WC_Order $order Order instance.
*/
private function log_event( $user_id, $action_key, $description, $status, $order ) {
$activity_name = $this->activity_types['woocommerce'][ $action_key ] ?? ucfirst( str_replace( '_', ' ', $action_key ) );
$order_link = method_exists( $order, 'get_edit_order_url' ) ? $order->get_edit_order_url() : '';
$this->logger->insert_log(
$user_id,
'WooCommerce',
$activity_name,
$description,
$status,
$order_link ?: ''
);
}
}