[ $this, 'get_chart_data' ], 'vv_get_stats' => [ $this, 'get_stats' ], // Products 'vv_save_product' => [ $this, 'save_product' ], 'vv_delete_product' => [ $this, 'delete_product' ], 'vv_toggle_product' => [ $this, 'toggle_product' ], 'vv_bulk_product_action' => [ $this, 'bulk_product_action' ], // Orders 'vv_update_order_status' => [ $this, 'update_order_status' ], // Withdrawal 'vv_request_withdrawal' => [ $this, 'request_withdrawal' ], 'vv_save_bank' => [ $this, 'save_bank' ], // Reviews 'vv_submit_review' => [ $this, 'submit_review' ], 'vv_vendor_reply' => [ $this, 'vendor_reply' ], // Messages 'vv_send_message' => [ $this, 'send_message' ], 'vv_poll_messages' => [ $this, 'poll_messages' ], // Follow 'vv_toggle_follow' => [ $this, 'toggle_follow' ], // Image upload 'vv_upload_image' => [ $this, 'upload_image' ], ]; foreach ( $actions as $action => $callback ) { add_action( 'wp_ajax_' . $action, $callback ); } // Public AJAX (non-logged-in) add_action( 'wp_ajax_nopriv_vv_poll_messages', '__return_false' ); } // ── Helpers ────────────────────────────────────────────────────────────── private function verify( $action = 'vv_ajax' ) { if ( ! check_ajax_referer( $action, 'nonce', false ) ) { wp_send_json_error( [ 'message' => __( 'Security check failed.', 'vendorverse' ) ], 403 ); } } private function require_vendor() { if ( ! is_user_logged_in() || ! VV_Helpers::is_vendor() ) { wp_send_json_error( [ 'message' => __( 'Vendor access required.', 'vendorverse' ) ], 403 ); } } private function require_login() { if ( ! is_user_logged_in() ) { wp_send_json_error( [ 'message' => __( 'Please log in.', 'vendorverse' ) ], 401 ); } } // ── Dashboard ──────────────────────────────────────────────────────────── public function get_chart_data() { $this->verify(); $this->require_vendor(); $days = min( 365, max( 7, (int) ( $_POST['days'] ?? 30 ) ) ); $data = VV_Helpers::sales_chart_data( get_current_user_id(), $days ); wp_send_json_success( $data ); } public function get_stats() { $this->verify(); $this->require_vendor(); $stats = VV_Helpers::get_vendor_stats( get_current_user_id() ); wp_send_json_success( $stats ); } // ── Products ───────────────────────────────────────────────────────────── public function save_product() { $this->verify(); $this->require_vendor(); $user_id = get_current_user_id(); $pid = (int) ( $_POST['pid'] ?? 0 ); // Validate $name = sanitize_text_field( $_POST['product_name'] ?? '' ); $price = (float) ( $_POST['price'] ?? 0 ); if ( ! $name ) wp_send_json_error( [ 'message' => 'Product name is required.' ] ); if ( $price <= 0 ) wp_send_json_error( [ 'message' => 'Price must be greater than 0.' ] ); // Check subscription limit for new products if ( ! $pid ) { $current = (int) count( get_posts( [ 'post_type' => 'product', 'post_status' => [ 'publish', 'pending', 'draft' ], 'post_author' => $user_id, 'fields' => 'ids' ] ) ); $max = VV_Subscription::max_products( $user_id ); if ( $current >= $max ) { wp_send_json_error( [ 'message' => "Product limit reached ($current/$max). Upgrade your plan." ] ); } } if ( $pid ) { // Edit if ( (int) get_post_field( 'post_author', $pid ) !== $user_id ) wp_send_json_error( [ 'message' => 'Access denied.' ] ); $product = wc_get_product( $pid ); } else { $product = new WC_Product_Simple(); } $product->set_name( $name ); $product->set_description( wp_kses_post( $_POST['full_description'] ?? '' ) ); $product->set_short_description( wp_kses_post( $_POST['short_description'] ?? '' ) ); $product->set_regular_price( $price ); if ( ! empty( $_POST['sale_price'] ) ) $product->set_sale_price( (float) $_POST['sale_price'] ); $product->set_status( get_option( 'vv_auto_approve_products' ) === 'yes' ? 'publish' : 'pending' ); $type = sanitize_text_field( $_POST['product_type'] ?? 'virtual' ); if ( $type === 'downloadable' ) { $product->set_downloadable( true ); $product->set_virtual( false ); } else { $product->set_virtual( true ); $product->set_downloadable( false ); } $new_pid = $product->save(); wp_update_post( [ 'ID' => $new_pid, 'post_author' => $user_id ] ); // Tags & category if ( ! empty( $_POST['tags'] ) ) { wp_set_object_terms( $new_pid, array_map( 'trim', explode( ',', sanitize_text_field( $_POST['tags'] ) ) ), 'product_tag' ); } if ( ! empty( $_POST['category'] ) ) { $term = get_term_by( 'name', sanitize_text_field( $_POST['category'] ), 'product_cat' ); if ( $term ) wp_set_object_terms( $new_pid, $term->term_id, 'product_cat' ); } update_post_meta( $new_pid, '_vv_terms', wp_kses_post( $_POST['terms_condtion'] ?? '' ) ); wp_set_object_terms( $new_pid, 'simple', 'product_type' ); // Handle image if ( ! empty( $_FILES['product_image']['name'] ) ) { $img_id = $this->handle_upload( 'product_image' ); if ( $img_id && ! is_wp_error( $img_id ) ) set_post_thumbnail( $new_pid, $img_id ); } // Approval queue if ( ! $pid && get_option( 'vv_auto_approve_products' ) !== 'yes' ) { global $wpdb; $wpdb->insert( "{$wpdb->prefix}vv_product_approvals", [ 'product_id' => $new_pid, 'vendor_id' => $user_id, 'status' => 'pending', ] ); } VV_Helpers::flush_vendor_cache( $user_id ); wp_send_json_success( [ 'message' => $pid ? 'Product updated! Pending review.' : 'Product submitted for review!', 'product_id' => $new_pid, 'status' => 'pending', ] ); } public function delete_product() { $this->verify(); $this->require_vendor(); $pid = (int) ( $_POST['pid'] ?? 0 ); $user_id = get_current_user_id(); if ( (int) get_post_field( 'post_author', $pid ) !== $user_id ) { wp_send_json_error( [ 'message' => 'Access denied.' ] ); } wp_delete_post( $pid, true ); VV_Helpers::flush_vendor_cache( $user_id ); wp_send_json_success( [ 'message' => 'Product deleted.' ] ); } public function toggle_product() { $this->verify(); $this->require_vendor(); $pid = (int) ( $_POST['pid'] ?? 0 ); $status = sanitize_text_field( $_POST['status'] ?? 'draft' ); $allowed = [ 'publish', 'draft', 'pending' ]; $user_id = get_current_user_id(); if ( ! in_array( $status, $allowed ) ) wp_send_json_error( [ 'message' => 'Invalid status.' ] ); if ( (int) get_post_field( 'post_author', $pid ) !== $user_id ) wp_send_json_error( [ 'message' => 'Access denied.' ] ); // If publishing, need approval unless auto-approve if ( $status === 'publish' && get_option( 'vv_auto_approve_products' ) !== 'yes' ) $status = 'pending'; wp_update_post( [ 'ID' => $pid, 'post_status' => $status ] ); wp_send_json_success( [ 'status' => $status, 'message' => 'Product status updated.' ] ); } public function bulk_product_action() { $this->verify(); $this->require_vendor(); $ids = array_map( 'intval', (array) ( $_POST['ids'] ?? [] ) ); $action = sanitize_text_field( $_POST['bulk_action'] ?? '' ); $user_id = get_current_user_id(); $count = 0; foreach ( $ids as $pid ) { if ( (int) get_post_field( 'post_author', $pid ) !== $user_id ) continue; if ( $action === 'delete' ) { wp_delete_post( $pid, true ); $count++; } elseif ( $action === 'publish' ) { wp_update_post( [ 'ID' => $pid, 'post_status' => 'pending' ] ); $count++; } elseif ( $action === 'unpublish' ) { wp_update_post( [ 'ID' => $pid, 'post_status' => 'draft' ] ); $count++; } } VV_Helpers::flush_vendor_cache( $user_id ); wp_send_json_success( [ 'count' => $count, 'message' => "$count products updated." ] ); } // ── Orders ─────────────────────────────────────────────────────────────── public function update_order_status() { $this->verify(); $this->require_vendor(); $oid = (int) ( $_POST['order_id'] ?? 0 ); $status = sanitize_text_field( $_POST['status'] ?? '' ); $user_id = get_current_user_id(); global $wpdb; $owns = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}vv_system_orders WHERE order_id=%d AND product_owner_user_id=%d", $oid, $user_id ) ); if ( ! $owns ) wp_send_json_error( [ 'message' => 'Access denied.' ] ); $allowed = [ 'pending', 'processing', 'on-hold', 'completed', 'cancelled', 'refunded', 'failed' ]; if ( ! in_array( $status, $allowed ) ) wp_send_json_error( [ 'message' => 'Invalid status.' ] ); $order = wc_get_order( $oid ); if ( ! $order ) wp_send_json_error( [ 'message' => 'Order not found.' ] ); $order->update_status( $status ); wp_send_json_success( [ 'message' => 'Status updated to ' . $status . '.', 'status' => $status ] ); } // ── Withdrawal ─────────────────────────────────────────────────────────── public function request_withdrawal() { $this->verify(); $this->require_vendor(); $user_id = get_current_user_id(); $amount = (float) ( $_POST['amount'] ?? 0 ); $method = sanitize_text_field( $_POST['method'] ?? 'bank' ); $balance = VV_Helpers::get_vendor_balance( $user_id ); $min = (float) get_option( 'vv_min_withdrawal', 20 ); if ( $amount <= 0 ) wp_send_json_error( [ 'message' => 'Invalid amount.' ] ); if ( $amount < $min ) wp_send_json_error( [ 'message' => 'Minimum withdrawal is ' . wc_price( $min ) . '.' ] ); if ( $amount > $balance ) wp_send_json_error( [ 'message' => 'Insufficient balance.' ] ); $bank_details = []; foreach ( [ 'account_name', 'account_no', 'bank_name', 'ifsc', 'iban', 'bic' ] as $f ) { $bank_details[ $f ] = get_user_meta( $user_id, 'vv_' . $f === 'account_name' ? 'vv_bank_account_name' : 'vv_' . $f, true ); } // Proper meta keys $bank_details = [ 'account_name' => get_user_meta( $user_id, 'vv_bank_account_name', true ), 'account_no' => get_user_meta( $user_id, 'vv_bank_account', true ), 'bank_name' => get_user_meta( $user_id, 'vv_name_of_bank', true ), 'ifsc' => get_user_meta( $user_id, 'vv_ifsc_code', true ), 'iban' => get_user_meta( $user_id, 'vv_iban', true ), 'bic' => get_user_meta( $user_id, 'vv_bic', true ), ]; global $wpdb; VV_Helpers::record_transaction( $user_id, $amount, 'debit', "Withdrawal request — $method" ); $wpdb->insert( "{$wpdb->prefix}vv_withdrawals", [ 'vendor_id' => $user_id, 'amount' => $amount, 'method' => $method, 'account_details' => wp_json_encode( $bank_details ), 'status' => 'pending', ] ); VV_Helpers::flush_vendor_cache( $user_id ); wp_send_json_success( [ 'message' => 'Withdrawal request submitted!', 'new_balance' => VV_Helpers::get_vendor_balance( $user_id ), ] ); } public function save_bank() { $this->verify(); $this->require_login(); $user_id = get_current_user_id(); if ( get_user_meta( $user_id, 'vv_lock_bank', true ) ) { wp_send_json_error( [ 'message' => 'Bank details are locked. Contact admin to update.' ] ); } $fields = [ 'vv_bank_account_name', 'vv_bank_account', 'vv_name_of_bank', 'vv_ifsc_code', 'vv_iban', 'vv_bic' ]; foreach ( $fields as $f ) { update_user_meta( $user_id, $f, sanitize_text_field( $_POST[ $f ] ?? '' ) ); } if ( ! empty( $_POST['vv_lock_bank'] ) ) update_user_meta( $user_id, 'vv_lock_bank', 1 ); wp_send_json_success( [ 'message' => 'Bank details saved successfully!' ] ); } // ── Reviews ────────────────────────────────────────────────────────────── public function submit_review() { $this->verify(); $this->require_login(); $vid = (int) ( $_POST['vendor_id'] ?? 0 ); $oid = (int) ( $_POST['order_id'] ?? 0 ); $rating = min( 5, max( 1, (int) ( $_POST['rating'] ?? 5 ) ) ); $review = sanitize_textarea_field( $_POST['review'] ?? '' ); $customer = get_current_user_id(); if ( ! $vid || ! $review ) wp_send_json_error( [ 'message' => 'All fields are required.' ] ); global $wpdb; $exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}vv_vendor_reviews WHERE vendor_id=%d AND customer_id=%d AND order_id=%d", $vid, $customer, $oid ) ); if ( $exists ) wp_send_json_error( [ 'message' => 'You have already reviewed this vendor.' ] ); $wpdb->insert( "{$wpdb->prefix}vv_vendor_reviews", [ 'vendor_id' => $vid, 'customer_id' => $customer, 'order_id' => $oid, 'rating' => $rating, 'review' => $review, 'is_verified' => $oid ? 1 : 0, ] ); VV_Helpers::flush_vendor_cache( $vid ); $user = wp_get_current_user(); wp_send_json_success( [ 'message' => 'Review submitted. Thank you!', 'display_name' => $user->display_name, 'rating' => $rating, 'review' => $review, ] ); } public function vendor_reply() { $this->verify(); $this->require_vendor(); $rid = (int) ( $_POST['review_id'] ?? 0 ); $reply = sanitize_textarea_field( $_POST['reply'] ?? '' ); $vendor = get_current_user_id(); if ( ! $rid || ! $reply ) wp_send_json_error( [ 'message' => 'Reply text required.' ] ); global $wpdb; $result = $wpdb->update( "{$wpdb->prefix}vv_vendor_reviews", [ 'vendor_response' => $reply ], [ 'id' => $rid, 'vendor_id' => $vendor ] ); if ( ! $result ) wp_send_json_error( [ 'message' => 'Could not save reply.' ] ); wp_send_json_success( [ 'message' => 'Reply saved.', 'reply' => $reply ] ); } // ── Messages ───────────────────────────────────────────────────────────── public function send_message() { $this->verify(); $this->require_login(); $sender = get_current_user_id(); $receiver = (int) ( $_POST['receiver_id'] ?? 0 ); $message = sanitize_textarea_field( $_POST['message'] ?? '' ); $conv = (int) ( $_POST['conversation_id'] ?? 0 ); $oid = (int) ( $_POST['order_id'] ?? 0 ); if ( ! $receiver || ! $message ) wp_send_json_error( [ 'message' => 'Missing fields.' ] ); if ( ! $conv ) { $a = min($sender,$receiver); $b = max($sender,$receiver); $conv = crc32("$a-$b"); } global $wpdb; $wpdb->insert( "{$wpdb->prefix}vv_messages", [ 'conversation_id' => $conv, 'sender_id' => $sender, 'receiver_id' => $receiver, 'order_id' => $oid, 'message' => $message, ] ); do_action( 'vv_new_message', $receiver, $sender, $message ); $user = wp_get_current_user(); wp_send_json_success( [ 'id' => $wpdb->insert_id, 'message' => $message, 'sender' => $user->display_name, 'time' => current_time( 'g:ia' ), ] ); } public function poll_messages() { if ( ! is_user_logged_in() ) wp_send_json_error( [], 401 ); check_ajax_referer( 'vv_ajax', 'nonce' ); $uid = get_current_user_id(); $conv = (int) ( $_POST['conv'] ?? 0 ); $since = (int) ( $_POST['last_id'] ?? 0 ); global $wpdb; $rows = $wpdb->get_results( $wpdb->prepare( "SELECT m.*, u.display_name FROM {$wpdb->prefix}vv_messages m LEFT JOIN {$wpdb->prefix}users u ON m.sender_id=u.ID WHERE m.conversation_id=%d AND m.id > %d ORDER BY m.created_at ASC LIMIT 50", $conv, $since ) ); // Mark as read $wpdb->update( "{$wpdb->prefix}vv_messages", ['is_read'=>1], ['conversation_id'=>$conv,'receiver_id'=>$uid] ); wp_send_json_success( [ 'messages' => $rows, 'current_user' => $uid ] ); } // ── Follow ─────────────────────────────────────────────────────────────── public function toggle_follow() { $this->verify(); $this->require_login(); $vid = (int) ( $_POST['vendor_id'] ?? 0 ); $customer = get_current_user_id(); if ( ! $vid ) wp_send_json_error( [ 'message' => 'Invalid vendor.' ] ); global $wpdb; $existing = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}vv_followers WHERE vendor_id=%d AND customer_id=%d", $vid, $customer ) ); if ( $existing ) { $wpdb->delete( "{$wpdb->prefix}vv_followers", [ 'vendor_id' => $vid, 'customer_id' => $customer ] ); $following = false; } else { $wpdb->insert( "{$wpdb->prefix}vv_followers", [ 'vendor_id' => $vid, 'customer_id' => $customer ] ); $following = true; } VV_Helpers::flush_vendor_cache( $vid ); $count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}vv_followers WHERE vendor_id=%d", $vid ) ); wp_send_json_success( [ 'following' => $following, 'count' => $count ] ); } // ── Image upload ───────────────────────────────────────────────────────── public function upload_image() { $this->verify(); $this->require_vendor(); if ( empty( $_FILES['file'] ) ) wp_send_json_error( [ 'message' => 'No file uploaded.' ] ); $id = $this->handle_upload( 'file' ); if ( is_wp_error( $id ) ) wp_send_json_error( [ 'message' => $id->get_error_message() ] ); wp_send_json_success( [ 'id' => $id, 'url' => wp_get_attachment_url( $id ) ] ); } private function handle_upload( $input ) { if ( ! function_exists( 'media_handle_upload' ) ) { require_once ABSPATH . 'wp-admin/includes/image.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; } $allowed = VV_Helpers::allowed_file_types(); $file = $_FILES[ $input ]; $ext = strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) ); $valid = false; foreach ( $allowed as $exts => $mime ) { if ( in_array( $ext, explode( '|', $exts ) ) ) { $valid = true; break; } } if ( ! $valid ) return new WP_Error( 'invalid_file', 'File type not allowed.' ); return media_handle_upload( $input, 0 ); } } new VV_Ajax();