![]() Server : Apache/2.4.52 (Ubuntu) System : Linux webserver 6.8.0-49-generic #49~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Wed Nov 6 17:42:15 UTC 2 x86_64 User : www-data ( 33) PHP Version : 8.1.2-1ubuntu2.21 Disable Function : NONE Directory : /var/www/theprintave/wp-content/plugins/dokan-lite/includes/Order/ |
<?php namespace WeDevs\Dokan\Order; use Exception; use WC_Order; // don't call the file directly if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Admin Hooks * * @since 3.0.0 * * @package dokan * * @author weDevs */ class Hooks { /** * Load automatically when class initiate * * @since 3.0.0 * * @return void */ public function __construct() { // on order status change add_action( 'woocommerce_order_status_changed', [ $this, 'on_order_status_change' ], 10, 4 ); add_action( 'woocommerce_order_status_changed', [ $this, 'manage_refunded_for_order' ], 15, 4 ); add_action( 'woocommerce_order_status_changed', [ $this, 'on_sub_order_change' ], 99, 4 ); // create sub-orders add_action( 'woocommerce_checkout_update_order_meta', [ $this, 'split_vendor_orders' ] ); add_action( 'woocommerce_store_api_checkout_order_processed', [ $this, 'split_vendor_orders' ] ); // order table synced for WooCommerce update order meta add_action( 'woocommerce_checkout_update_order_meta', 'dokan_sync_insert_order', 20 ); add_action( 'woocommerce_store_api_checkout_order_processed', 'dokan_sync_insert_order', 20 ); // order table synced for dokan update order meta add_action( 'dokan_checkout_update_order_meta', 'dokan_sync_insert_order' ); // prevent non-vendor coupons from being added add_filter( 'woocommerce_coupon_is_valid', [ $this, 'ensure_vendor_coupon' ], 10, 3 ); if ( is_admin() ) { add_action( 'woocommerce_process_shop_order_meta', 'dokan_sync_insert_order', 60 ); } // restore order stock if it's been reduced by twice add_action( 'woocommerce_reduce_order_stock', [ $this, 'restore_reduced_order_stock' ] ); add_action( 'woocommerce_reduce_order_stock', [ $this, 'handle_order_notes_for_suborder' ], 99 ); } /** * Update the child order status when a parent order status is changed * * @param int $order_id * @param string $old_status * @param string $new_status * @param WC_Order $order * * @return void */ public function on_order_status_change( $order_id, $old_status, $new_status, $order ) { global $wpdb; // Split order if the order doesn't have parent and sub orders, // and the order is created from dashboard. if ( empty( $order->get_parent_id() ) && empty( $order->get_meta( 'has_sub_order' ) ) && is_admin() ) { // Remove the hook to prevent recursive callas. remove_action( 'woocommerce_order_status_changed', [ $this, 'on_order_status_change' ], 10 ); // Split the order. dokan()->order->maybe_split_orders( $order_id ); // Add the hook back. add_action( 'woocommerce_order_status_changed', [ $this, 'on_order_status_change' ], 10, 4 ); } // make sure order status contains "wc-" prefix if ( stripos( $new_status, 'wc-' ) === false ) { $new_status = 'wc-' . $new_status; } // insert on dokan sync table $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->dokan_orders, [ 'order_status' => $new_status ], [ 'order_id' => $order_id ], [ '%s' ], [ '%d' ] ); // Update sub-order statuses $sub_orders = dokan()->order->get_child_orders( $order_id ); if ( $sub_orders ) { foreach ( $sub_orders as $sub_order ) { if ( is_callable( [ $sub_order, 'update_status' ] ) ) { $current_status = $sub_order->get_status(); if ( $this->is_status_change_allowed( $current_status, $new_status ) ) { $sub_order->update_status( $new_status ); } else { $this->log_skipped_status_update( $sub_order->get_id(), $current_status, $new_status ); } } } } /** * If `exclude_cod_payment` is enabled, don't include the fund in vendor's withdrawal balance. * * @since 3.8.3 */ $exclude_cod_payment = 'on' === dokan_get_option( 'exclude_cod_payment', 'dokan_withdraw', 'off' ); if ( $exclude_cod_payment && 'cod' === $order->get_payment_method() ) { return; } // update on vendor-balance table $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->dokan_vendor_balance, [ 'status' => $new_status ], [ 'trn_id' => $order_id, 'trn_type' => 'dokan_orders', ], [ '%s' ], [ '%d', '%s' ] ); } /** * Check if a status change is allowed for a sub-order. * * This method determines whether a sub-order can transition from its current status * to a new status, based on a configurable whitelist of allowed transitions. * * @since 3.12.2 * * @param string $current_status The current status of the sub-order (should include 'wc-' prefix). * @param string $new_status The new status to check (should include 'wc-' prefix). * * @return bool True if the status change is allowed, false otherwise. */ private function is_status_change_allowed( string $current_status, string $new_status ): bool { // Ensure both statuses have 'wc-' prefix $current_status = $this->maybe_add_wc_prefix( $current_status ); $new_status = $this->maybe_add_wc_prefix( $new_status ); // Define the default whitelist of allowed status transitions $default_whitelist = [ 'wc-pending' => [ 'any' ], 'wc-on-hold' => [ 'wc-pending', 'wc-on-hold', 'wc-processing', 'wc-completed', 'wc-failed' ], 'wc-processing' => [ 'wc-completed', 'wc-failed', 'wc-cancelled', 'wc-refunded' ], 'wc-completed' => [ 'wc-refunded' ], 'wc-failed' => [ 'wc-pending', 'wc-on-hold', 'wc-processing', 'wc-failed', 'wc-cancelled' ], 'wc-cancelled' => [], 'wc-refunded' => [], ]; /** * Filter the whitelist of allowed status transitions for sub-orders. * * This filter allows developers to customize the whitelist that determines * which status transitions are allowed for sub-orders when the main order * status is updated. By modifying this whitelist, you can control how * sub-order statuses are updated in relation to the main order. * * @since 3.12.2 * * @param array $whitelist An associative array where keys are current statuses * and values are arrays of allowed new statuses. * The special value 'any' allows transition to any status. * * @return array Modified whitelist of allowed status transitions. */ $whitelist = apply_filters( 'dokan_sub_order_status_update_whitelist', $default_whitelist ); // Allow any status change if the current status is not in the whitelist or the new status is not allowed if ( ! array_key_exists( $current_status, $whitelist ) || ! array_key_exists( $new_status, $whitelist ) ) { return true; } // If 'any' is allowed for the current status, all transitions are allowed if ( in_array( 'any', $whitelist[ $current_status ], true ) ) { return true; } // Check if the new status is in the list of allowed transitions return in_array( $new_status, $whitelist[ $current_status ], true ); } /** * Ensure a status string has the 'wc-' prefix. * * @since 3.12.2 * * @param string $status The status string to check. * * @return string The status string with 'wc-' prefix added if it was missing. */ private function maybe_add_wc_prefix( string $status ): string { return strpos( $status, 'wc-' ) === 0 ? $status : 'wc-' . $status; } /** * Log a skipped status update for a sub-order. * * This method logs a message to the error log when a status update for a sub-order * is skipped because the status change is not allowed. * * @since 3.12.2 * * @param int $order_id The ID of the sub-order. * @param string $current_status The current status of the sub-order. * @param string $new_status The new status that was not allowed. * * @return void */ private function log_skipped_status_update( int $order_id, string $current_status, string $new_status ) { dokan_log( sprintf( 'Dokan: Skipped status update for sub-order %d from %s to %s', $order_id, $current_status, $new_status ) ); } /** * If order status is set to refunded from vendor dashboard, enter remaining balance into vendor balance table. * * @since 3.8.0 Created this method from on_order_status_change() * * @param int $order_id * @param string $old_status * @param string $new_status * @param WC_Order $order * * @return void */ public function manage_refunded_for_order( $order_id, $old_status, $new_status, $order ) { global $wpdb; // verify nonce if ( ! isset( $_POST['_wpnonce'], $_POST['post_type'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ), 'dokan_change_status' ) ) { return; } $exclude_cod_payment = 'on' === dokan_get_option( 'exclude_cod_payment', 'dokan_withdraw', 'off' ); if ( $exclude_cod_payment && 'cod' === $order->get_payment_method() ) { return; } if ( $new_status !== 'wc-refunded' ) { return; } if ( $_POST['post_type'] !== 'shop_order' ) { return; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $balance_data = $wpdb->get_var( $wpdb->prepare( "SELECT 1 FROM $wpdb->dokan_vendor_balance WHERE trn_id = %d AND trn_type = %s AND status = 'approved'", [ $order_id, 'dokan_refund' ] ) ); if ( ! empty( $balance_data ) ) { return; } $seller_id = dokan_get_seller_id_by_order( $order_id ); $net_amount = dokan()->commission->get_earning_by_order( $order ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->insert( $wpdb->dokan_vendor_balance, [ 'vendor_id' => $seller_id, 'trn_id' => $order_id, 'trn_type' => 'dokan_refund', 'debit' => 0, 'credit' => $net_amount, 'status' => 'approved', 'trn_date' => current_time( 'mysql' ), 'balance_date' => current_time( 'mysql' ), ], [ '%d', '%d', '%s', '%f', '%f', '%s', '%s', '%s', ] ); // update the order table with new refund amount $order_data = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( "select * from $wpdb->dokan_orders where order_id = %d", $order_id ) ); if ( isset( $order_data->order_total, $order_data->net_amount ) ) { // insert on dokan sync table $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->dokan_orders, [ 'order_total' => 0, 'net_amount' => 0, ], [ 'order_id' => $order_id, ], [ '%f', '%f', ], [ '%d', ] ); } } /** * Mark the parent order as complete when all the child order are completed * * @param integer $order_id * @param string $old_status * @param string $new_status * @param WC_Order $order * * @return void */ public function on_sub_order_change( $order_id, $old_status, $new_status, $order ) { // we are monitoring only child orders if ( $order->get_parent_id() === 0 ) { return; } // get all the child orders and monitor the status $parent_order_id = $order->get_parent_id(); $sub_orders = dokan()->order->get_child_orders( $parent_order_id ); if ( ! $sub_orders ) { return; } // return if any child order is not completed $all_complete = true; // Exclude manual gateways from auto complete order status update for digital products. $excluded_gateways = apply_filters( 'dokan_excluded_payment_gateways_on_order_status_update', array( 'bacs', 'cheque', 'cod' ) ); foreach ( $sub_orders as $sub_order ) { // if the order is a downloadable and virtual product, then we need to set the status to complete if ( 'processing' === $sub_order->get_status() && $order->is_paid() && ! in_array( $order->get_payment_method(), $excluded_gateways, true ) && ! $sub_order->needs_processing() ) { $sub_order->set_status( 'completed', __( 'Marked as completed because it contains digital products only.', 'dokan-lite' ) ); $sub_order->save(); } // if any child order is not completed, break the loop if ( $sub_order->get_status() !== 'completed' ) { $all_complete = false; } } // seems like all the child orders are completed // mark the parent order as complete if ( $all_complete ) { $parent_order = wc_get_order( $parent_order_id ); $parent_order->update_status( 'wc-completed', __( 'Mark parent order completed when all child orders are completed.', 'dokan-lite' ) ); } } /** * Split order for vendor * * @since 3.0.0 * * @param $parent_order_id * * @return void */ public function split_vendor_orders( $parent_order_id ) { dokan()->order->maybe_split_orders( $parent_order_id ); } /** * Ensure vendor coupon * * For consistency, restrict coupons in cart if only * products from that vendor exists in the cart. Also, a coupon * should be restricted with a product. * * For example: When entering a coupon created by admin is applied, make * sure a product of the admin is in the cart. Otherwise it wouldn't be * possible to distribute the coupon in sub orders. * * @param boolean $valid * @param \WC_Coupon $coupon * @param \WC_Discounts $discount * * @throws Exception * @return boolean|Exception */ public function ensure_vendor_coupon( $valid, $coupon, $discount ) { $available_vendors = []; $available_products = []; if ( WC()->cart && ! WC()->cart->is_empty() ) { foreach ( WC()->cart->get_cart() as $item ) { $product_id = $item['data']->get_id(); $available_vendors[] = (int) dokan_get_vendor_by_product( $product_id, true ); $available_products[] = $product_id; } } else { foreach ( $discount->get_items() as $item ) { if ( ! isset( $item->product ) || ! $item->product instanceof \WC_Product ) { continue; } $item_id = $item->product->get_id(); $available_vendors[] = (int) dokan_get_vendor_by_product( $item_id, true ); $available_products[] = $item_id; } } $available_vendors = array_unique( $available_vendors ); if ( $coupon->is_type( 'fixed_cart' ) && count( $available_vendors ) > 1 ) { throw new Exception( esc_html__( 'This coupon is invalid for multiple vendors.', 'dokan-lite' ) ); } // Make sure applied coupon created by admin if ( apply_filters( 'dokan_ensure_admin_have_create_coupon', $valid, $coupon, $available_vendors, $available_products ) ) { return true; } if ( ! apply_filters( 'dokan_ensure_vendor_coupon', true ) ) { return $valid; } // A coupon must be bound with a product if ( ! dokan()->is_pro_exists() && count( $coupon->get_product_ids() ) === 0 ) { throw new Exception( esc_html__( 'A coupon must be restricted with a vendor product.', 'dokan-lite' ) ); } $coupon_id = $coupon->get_id(); $vendor_id = intval( get_post_field( 'post_author', $coupon_id ) ); if ( ! in_array( $vendor_id, $available_vendors, true ) ) { return false; } return $valid; } /** * Restore order stock if it's been reduced by twice * * @param WC_Order $order * * @return void */ public function restore_reduced_order_stock( $order ) { // seems in rest request, there is no such issue like (stock reduced by twice), so return early if ( defined( 'REST_REQUEST' ) ) { return; } // seems it's not a parent order so return early if ( ! $order->get_meta( 'has_sub_order' ) ) { return; } // Loop over all items. foreach ( $order->get_items( 'line_item' ) as $item ) { // Only reduce stock once for each item. $product = $item->get_product(); $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); if ( ! $item_stock_reduced || ! $product || ! $product->managing_stock() ) { continue; } $item_name = $product->get_formatted_name(); $new_stock = wc_update_product_stock( $product, $item_stock_reduced, 'increase' ); if ( is_wp_error( $new_stock ) ) { /* translators: %s item name. */ $order->add_order_note( sprintf( __( 'Unable to restore stock for item %s.', 'dokan-lite' ), $item_name ) ); continue; } $item->delete_meta_data( '_reduced_stock' ); $item->save(); } } /** * Handle stock level wrong calculation in order notes for suborder * * @since 3.8.3 * * @param WC_Order $order * * @return void */ public function handle_order_notes_for_suborder( $order ) { //return if it has suborder. only continue if this is a suborder if ( ! $order->get_meta( 'has_sub_order' ) ) { return; } $notes = wc_get_order_notes( [ 'order_id' => $order->get_id() ] ); //change stock level note status instead of deleting foreach ( $notes as $note ) { //here using the woocommerce as text domain because we are using woocommerce text for searching if ( false !== strpos( $note->content, __( 'Stock levels reduced:', 'woocommerce' ) ) ) { //phpcs:ignore WordPress.WP.I18n.TextDomainMismatch //update notes status to `hold`, so that it will not show in order details page wp_set_comment_status( $note->id, 'hold' ); } } //adding stock level notes in order foreach ( $order->get_items( 'line_item' ) as $key => $line_item ) { $product = $line_item->get_product(); if ( $product->get_manage_stock() ) { $stock_quantity = $product->get_stock_quantity(); $previous_quantity = (int) $stock_quantity + $line_item->get_quantity(); $notes_content = $product->get_formatted_name() . ' ' . $previous_quantity . '→' . $stock_quantity; //here using the woocommerce as text domain because we are using woocommerce text for adding $order->add_order_note( __( 'Stock levels reduced:', 'woocommerce' ) . ' ' . $notes_content ); //phpcs:ignore WordPress.WP.I18n.TextDomainMismatch } } } }