*/ private const TRACKED_STATUSES = array( 'completed', 'processing' ); /** * Registers WordPress hooks for this interceptor. * * @return void */ public function register_hooks(): void { add_action( 'woocommerce_order_status_changed', array( $this, 'action_order_status_changed' ), 10, 3 ); } /** * Sync order data when WC status changes to a tracked state. * * @param int $order_id Order ID. * @param string $old_status Old status (unused). * @param string $new_status New status. * @return void */ public function action_order_status_changed( int $order_id, string $old_status, string $new_status ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed if ( ! $this->is_active() ) { return; } if ( ! in_array( $new_status, self::TRACKED_STATUSES, true ) ) { return; } try { $this->sync_commission( $order_id, $new_status ); } catch ( \Throwable $e ) { TMDO_Logger::error( $this->module, 'woocommerce_order_status_changed', $e->getMessage() ); } } /** * INSERT/UPDATE wp_wpdo_wc_commissions for the given order. * * Skips orders without a `_hp_vendor` meta — only marketplace orders count. * * @param int $wc_order_id WooCommerce order ID. * @param string $status Order status. * @return void */ private function sync_commission( int $wc_order_id, string $status ): void { if ( ! function_exists( 'wc_get_order' ) ) { return; } $order = wc_get_order( $wc_order_id ); if ( ! $order ) { return; } $vendor_id = (int) $order->get_meta( '_hp_vendor' ); if ( $vendor_id <= 0 ) { // Not a marketplace order — skip. return; } $listing_id = (int) $order->get_meta( '_hp_listing' ); $subtotal = (float) ( $order->get_meta( '_hp_subtotal' ) ?: 0 ); $commission = (float) ( $order->get_meta( '_hp_commission' ) ?: 0 ); $vendor_payout = (float) ( $order->get_meta( '_hp_vendor_payout' ) ?: 0 ); $commission_rate = (float) ( $order->get_meta( '_hp_commission_rate' ) ?: 0 ); $hpos_enabled = class_exists( 'TMDO_WooCommerce' ) && TMDO_WooCommerce::is_hpos_enabled() ? 1 : 0; $table = TMDO_DB::table( 'wpdo_wc_commissions' ); $now = TMDO_DB::now(); // v2.1.2 race-condition fix: atomic upsert via UNIQUE KEY ui_order_vendor. // Replaces SELECT-then-INSERT/UPDATE which could race two concurrent // status-change events (e.g. gateway IPN + admin click) and trigger // duplicate-key DB errors. Single round-trip; on conflict only updates // the mutable columns (status / updated_at), preserves financials. TMDO_DB::upsert( $table, array( 'wc_order_id' => $wc_order_id, 'vendor_id' => $vendor_id, 'listing_id' => $listing_id, 'subtotal' => $subtotal, 'commission' => $commission, 'vendor_payout' => $vendor_payout, 'commission_rate' => $commission_rate, 'status' => $status, 'hpos_enabled' => $hpos_enabled, 'created_at' => $now, 'updated_at' => $now, ), array( 'status', 'updated_at' ), // columns to update on conflict. array( 'wc_order_id', 'vendor_id' ), // composite unique key. array( '%d', '%d', '%d', '%f', '%f', '%f', '%f', '%s', '%d', '%s', '%s' ) ); } /** * Aggregate commission summary for a vendor. * * @param int $vendor_id Vendor user ID. * @param string $status Optional status filter ('processing' / 'completed' / 'all'). * @return array{total_subtotal:float, total_commission:float, total_payout:float, order_count:int} */ public static function vendor_summary( int $vendor_id, string $status = 'all' ): array { global $wpdb; $table = TMDO_DB::table( 'wpdo_wc_commissions' ); $where = $wpdb->prepare( ' WHERE vendor_id = %d ', $vendor_id ); if ( 'all' !== $status ) { $where .= $wpdb->prepare( ' AND status = %s ', $status ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared -- {$table} via TMDO_DB::table(); {$where} is a pre-validated SQL fragment. $row = $wpdb->get_row( "SELECT SUM(subtotal) AS total_subtotal, SUM(commission) AS total_commission, SUM(vendor_payout) AS total_payout, COUNT(*) AS order_count FROM `{$table}` {$where}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- {$table} via TMDO_DB::table(); {$where} is a pre-validated SQL fragment. ARRAY_A ); return array( 'total_subtotal' => (float) ( $row['total_subtotal'] ?? 0 ), 'total_commission' => (float) ( $row['total_commission'] ?? 0 ), 'total_payout' => (float) ( $row['total_payout'] ?? 0 ), 'order_count' => (int) ( $row['order_count'] ?? 0 ), ); } }