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 ?: '' ); } }