/** * B2BKing Subaccount Importer v16 (Multisite-ready) * * Custom admin menu page in WordPress. Select parent account via searchable dropdown * (shows name, email, company and B2BKing group), paste in email addresses * — new accounts are created and linked as subaccounts. * * Multisite: * - Already on this site → ERROR * - Exists in network but not here → WARNING (no auto-add) * - Does not exist → Created * * Subaccounts inherit the parent account's WP role and B2BKing group. * Permission: selectable via checkboxes (default: "buy" only). * Welcome email: optional (default: off). * B2BKing hook b2bking_after_subaccount_created is NOT fired * (causes unwanted conversation emails and billing data copying). * * Requires: WordPress 5.0+, WooCommerce 5.0+, B2BKing, PHP 7.2+ * Snippet Type: Functions (PHP) * Run on: Admin only */ // ============================================================ // 1. REGISTER ADMIN MENU // ============================================================ add_action( 'admin_menu', 'b2b_importer_register_menu' ); function b2b_importer_register_menu() { add_menu_page( 'Subaccount Import', 'Subaccount Import', 'manage_woocommerce', 'b2b-subaccount-import', 'b2b_importer_render_page', 'dashicons-groups', 58 ); } // ============================================================ // 2. HELPER – GET B2BKING GROUP NAME // ============================================================ function b2b_importer_get_group_name( $user_id ) { // Respect B2BKing's b2bking_group_key_name filter (may prefix key per site) $group_key = apply_filters( 'b2bking_group_key_name', 'b2bking_customergroup' ); $group_id = get_user_meta( $user_id, $group_key, true ); if ( ! empty( $group_id ) ) { $name = get_the_title( intval( $group_id ) ); // Multisite: group post may live on another site — fall back to ID return $name ?: 'Group #' . intval( $group_id ); } return ''; } // ============================================================ // 3. HELPER – GET PARENT ACCOUNT PRIMARY ROLE // ============================================================ function b2b_importer_get_parent_role( $parent_id ) { $parent_user = get_userdata( $parent_id ); if ( $parent_user && ! empty( $parent_user->roles ) ) { return $parent_user->roles[0]; } return 'customer'; } // ============================================================ // 4. HELPER – VALIDATE USER IS NOT A SUBACCOUNT // ============================================================ function b2b_importer_is_valid_parent( $user_id ) { $account_type = get_user_meta( $user_id, 'b2bking_account_type', true ); return ( $account_type !== 'subaccount' ); } // ============================================================ // 5. HELPER – GENERATE UNIQUE USERNAME (with safety limit) // ============================================================ function b2b_importer_generate_username( $email ) { // Note: usernames are checked but not reserved. In preview mode, the suggested // username may differ from the actual one at import time if another user is // created between preview and import. This is expected and harmless. $base = sanitize_user( strstr( $email, '@', true ), true ); if ( empty( $base ) ) { $base = 'user'; } // Multisite: username must be lowercase and without dots in some configs $base = strtolower( $base ); $username = $base; $suffix = 1; $max = 999; // Safety limit while ( username_exists( $username ) && $suffix <= $max ) { $username = $base . $suffix; $suffix++; } if ( $suffix > $max ) { return new WP_Error( 'username_exhausted', 'Could not generate unique username for ' . $email ); } return $username; } // ============================================================ // 6. HELPER – CONNECT SUBACCOUNT TO PARENT // ============================================================ function b2b_importer_connect_subaccount( $user_id, $parent_id, $permissions = array() ) { // Subaccount type and parent update_user_meta( $user_id, 'b2bking_account_type', 'subaccount' ); update_user_meta( $user_id, 'b2bking_account_parent', $parent_id ); // Inherit B2BKing group from parent // Respect B2BKing's b2bking_group_key_name filter (may prefix key per site) $group_key = apply_filters( 'b2bking_group_key_name', 'b2bking_customergroup' ); $parent_group = get_user_meta( $parent_id, $group_key, true ); $parent_b2b = get_user_meta( $parent_id, 'b2bking_b2buser', true ); if ( ! empty( $parent_group ) ) { update_user_meta( $user_id, $group_key, $parent_group ); } if ( ! empty( $parent_b2b ) ) { update_user_meta( $user_id, 'b2bking_b2buser', $parent_b2b ); } // Permissions — defaults with override from parameter $defaults = array( 'b2bking_account_permission_buy' => 1, 'b2bking_account_permission_buy_approval' => 0, 'b2bking_account_permission_view_orders' => 0, 'b2bking_account_permission_view_offers' => 0, 'b2bking_account_permission_view_conversations' => 0, 'b2bking_account_permission_view_lists' => 0, 'b2bking_account_permission_view_subscriptions' => 0, ); $merged = array_merge( $defaults, array_intersect_key( $permissions, $defaults ) ); foreach ( $merged as $key => $value ) { update_user_meta( $user_id, $key, intval( $value ) ? 1 : 0 ); } // Update parent's subaccounts list (duplicate protection) $current_list = get_user_meta( $parent_id, 'b2bking_subaccounts_list', true ); $ids_array = ! empty( $current_list ) ? array_filter( array_map( 'trim', explode( ',', $current_list ) ) ) : array(); if ( ! in_array( (string) $user_id, $ids_array, true ) ) { $ids_array[] = $user_id; update_user_meta( $parent_id, 'b2bking_subaccounts_list', implode( ',', $ids_array ) ); } // NOTE: We do NOT fire do_action('b2bking_after_subaccount_created') here. // B2BKing's own hook listener causes unwanted side effects: // 1. Copies parent account billing/shipping to the subaccount // 2. Creates an "Account activated" conversation + sends email // All required metadata is already set above — the hook is not needed. } // ============================================================ // 7. HELPER – CLEAR BILLING/SHIPPING FROM SUBACCOUNT // ============================================================ function b2b_importer_clear_billing_shipping( $user_id ) { $fields = array( // Billing 'billing_first_name', 'billing_last_name', 'billing_company', 'billing_address_1', 'billing_address_2', 'billing_city', 'billing_postcode', 'billing_country', 'billing_state', 'billing_phone', 'billing_email', // Shipping 'shipping_first_name', 'shipping_last_name', 'shipping_company', 'shipping_address_1', 'shipping_address_2', 'shipping_city', 'shipping_postcode', 'shipping_country', 'shipping_state', 'shipping_phone', ); foreach ( $fields as $key ) { delete_user_meta( $user_id, $key ); } } // ============================================================ // 8. HELPER – LOG MAIL FAILURES DURING IMPORT // ============================================================ function b2b_importer_log_mail_error( $wp_error ) { if ( is_wp_error( $wp_error ) ) { error_log( 'B2B Importer: wp_mail failed — ' . $wp_error->get_error_message() ); } } define( 'B2B_IMPORTER_MAX_EMAILS', 200 ); // ============================================================ // 9. HELPER – PARSE EMAIL LIST FROM RAW INPUT // ============================================================ function b2b_importer_parse_emails( $raw ) { $raw = wp_unslash( $raw ); // WordPress magic quotes $raw = sanitize_textarea_field( $raw ); // Support line breaks, commas and semicolons as separators $lines = preg_split( '/[\r\n,;]+/', $raw ); $lines = array_filter( array_map( 'trim', $lines ) ); // Limit emails per run (abuse/timeout protection) return array_slice( $lines, 0, B2B_IMPORTER_MAX_EMAILS ); } // ============================================================ // 10. HELPER – PARSE PERMISSIONS FROM POST DATA // ============================================================ function b2b_importer_parse_permissions( $raw_perms ) { // Allowed keys (whitelist) — ignore everything else $allowed = array( 'b2bking_account_permission_buy', 'b2bking_account_permission_buy_approval', 'b2bking_account_permission_view_orders', 'b2bking_account_permission_view_offers', 'b2bking_account_permission_view_conversations', 'b2bking_account_permission_view_lists', 'b2bking_account_permission_view_subscriptions', ); $parsed = array(); if ( is_array( $raw_perms ) ) { foreach ( $allowed as $key ) { $parsed[ $key ] = ! empty( $raw_perms[ $key ] ) ? 1 : 0; } } return $parsed; } // ============================================================ // 11. AJAX – SEARCH PARENT ACCOUNTS (Select2) // ============================================================ add_action( 'wp_ajax_b2b_search_parents', 'b2b_importer_search_parents' ); function b2b_importer_search_parents() { check_ajax_referer( 'b2b_importer_nonce', 'nonce' ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( 'Permission denied' ); } $term = sanitize_text_field( wp_unslash( $_GET['term'] ?? '' ) ); $term = mb_substr( $term, 0, 100 ); // Limit search term length if ( empty( $term ) ) { wp_send_json( array( 'results' => array() ) ); // Select2 format, wp_die() internally return; // Explicit — wp_send_json calls wp_die(), but return makes the flow clear } $args = array( 'search' => "*{$term}*", 'search_columns' => array( 'user_login', 'user_email', 'display_name' ), 'number' => 20, 'orderby' => 'display_name', 'blog_id' => get_current_blog_id(), 'meta_query' => array( 'relation' => 'OR', array( 'key' => 'b2bking_account_type', 'value' => 'subaccount', 'compare' => '!=', ), array( 'key' => 'b2bking_account_type', 'compare' => 'NOT EXISTS', ), ), ); $users = get_users( $args ); $results = array(); foreach ( $users as $user ) { $company = get_user_meta( $user->ID, 'billing_company', true ); $group_name = b2b_importer_get_group_name( $user->ID ); // Plain text — Select2 escapes via its built-in escapeMarkup $parts = array( $user->display_name, '(' . $user->user_email . ')', ); if ( $company ) { $parts[] = '— ' . $company; } if ( $group_name ) { $parts[] = '[' . $group_name . ']'; } $results[] = array( 'id' => $user->ID, 'text' => implode( ' ', $parts ), ); } // Select2 requires { results: [...] } format — wp_send_json_success() wraps in { success, data } // wp_send_json() calls wp_die() internally (WP 5.0+) wp_send_json( array( 'results' => $results ) ); } // ============================================================ // 12. AJAX – PREVIEW (Dry Run) // ============================================================ add_action( 'wp_ajax_b2b_preview_import', 'b2b_importer_preview' ); function b2b_importer_preview() { check_ajax_referer( 'b2b_importer_nonce', 'nonce' ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( 'Permission denied' ); } $parent_id = absint( wp_unslash( $_POST['parent_id'] ?? 0 ) ); $emails = b2b_importer_parse_emails( $_POST['emails'] ?? '' ); $permissions = b2b_importer_parse_permissions( $_POST['permissions'] ?? array() ); $send_welcome = ( absint( $_POST['send_welcome'] ?? 0 ) === 1 ); $clear_billing = ( absint( $_POST['clear_billing'] ?? 0 ) === 1 ); $hide_prices = ( absint( $_POST['hide_prices'] ?? 0 ) === 1 ); if ( ! $parent_id || ! get_userdata( $parent_id ) ) { wp_send_json_error( 'Invalid parent account.' ); } // Security check: parent must not be a subaccount if ( ! b2b_importer_is_valid_parent( $parent_id ) ) { wp_send_json_error( 'Selected account is a subaccount and cannot be a parent.' ); } $is_multisite = is_multisite(); $current_blog = get_current_blog_id(); $parent_role = b2b_importer_get_parent_role( $parent_id ); $group_name = b2b_importer_get_group_name( $parent_id ); $items = array(); foreach ( $emails as $email ) { $email = sanitize_email( $email ); if ( empty( $email ) || ! is_email( $email ) ) { $items[] = array( 'email' => $email ?: '(empty line)', 'status' => 'invalid', 'note' => 'Invalid email address.', ); continue; } $existing = get_user_by( 'email', $email ); if ( $existing ) { if ( $is_multisite && ! is_user_member_of_blog( $existing->ID, $current_blog ) ) { $items[] = array( 'email' => $email, 'status' => 'network_exists', 'note' => sprintf( 'Exists in the network (ID %d, %s) but not on this site. Requires manual action.', $existing->ID, $existing->user_login ), ); } else { $items[] = array( 'email' => $email, 'status' => 'exists', 'note' => sprintf( 'Already registered on this site (ID %d). Skipping.', $existing->ID ), ); } } else { $username = b2b_importer_generate_username( $email ); if ( is_wp_error( $username ) ) { $items[] = array( 'email' => $email, 'status' => 'invalid', 'note' => $username->get_error_message(), ); continue; } $items[] = array( 'email' => $email, 'status' => 'will_create', 'note' => sprintf( 'New account. Username: %s, Role: %s', $username, $parent_role ), 'username' => $username, ); } } $count_status = function( $status ) use ( $items ) { return count( array_filter( $items, function( $i ) use ( $status ) { return $i['status'] === $status; } ) ); }; // Build permissions summary for display $perm_labels = array( 'b2bking_account_permission_buy' => 'Place orders', 'b2bking_account_permission_buy_approval' => 'Orders require approval', 'b2bking_account_permission_view_orders' => 'View orders', 'b2bking_account_permission_view_offers' => 'View offers', 'b2bking_account_permission_view_conversations' => 'View conversations', 'b2bking_account_permission_view_lists' => 'View purchase lists', 'b2bking_account_permission_view_subscriptions' => 'View subscriptions', ); $active_perms = array(); foreach ( $permissions as $key => $val ) { if ( $val && isset( $perm_labels[ $key ] ) ) { $active_perms[] = $perm_labels[ $key ]; } } $perms_summary = ! empty( $active_perms ) ? implode( ', ', $active_perms ) : '(no permissions)'; wp_send_json_success( array( 'items' => $items, 'parent_role' => $parent_role, 'group_name' => $group_name ?: '(no B2BKing group)', 'permissions_summary' => $perms_summary, 'send_welcome' => $send_welcome, 'clear_billing' => $clear_billing, 'hide_prices' => $hide_prices, 'counts' => array( 'will_create' => $count_status( 'will_create' ), 'exists' => $count_status( 'exists' ), 'network_exists' => $count_status( 'network_exists' ), 'invalid' => $count_status( 'invalid' ), ), ) ); } // ============================================================ // 13. AJAX – IMPORT SUBACCOUNTS // ============================================================ add_action( 'wp_ajax_b2b_import_subaccounts', 'b2b_importer_import' ); function b2b_importer_import() { check_ajax_referer( 'b2b_importer_nonce', 'nonce' ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( 'Permission denied' ); } // Rate limit: max 5 import runs per minute per user $rate_key = 'b2b_import_rate_' . get_current_user_id(); $rate_count = (int) get_transient( $rate_key ); if ( $rate_count >= 5 ) { wp_send_json_error( 'Too many import attempts. Wait one minute.' ); } set_transient( $rate_key, $rate_count + 1, 60 ); $parent_id = absint( wp_unslash( $_POST['parent_id'] ?? 0 ) ); $emails = b2b_importer_parse_emails( $_POST['emails'] ?? '' ); $permissions = b2b_importer_parse_permissions( $_POST['permissions'] ?? array() ); $send_welcome = ( absint( $_POST['send_welcome'] ?? 0 ) === 1 ); $clear_billing = ( absint( $_POST['clear_billing'] ?? 0 ) === 1 ); $hide_prices = ( absint( $_POST['hide_prices'] ?? 0 ) === 1 ); if ( ! $parent_id || ! get_userdata( $parent_id ) ) { wp_send_json_error( 'Invalid parent account.' ); } // Security check: parent must not be a subaccount if ( ! b2b_importer_is_valid_parent( $parent_id ) ) { wp_send_json_error( 'Selected account is a subaccount and cannot be a parent.' ); } if ( empty( $emails ) ) { wp_send_json_error( 'No valid email addresses provided.' ); } $is_multisite = is_multisite(); $current_blog = get_current_blog_id(); $parent_role = b2b_importer_get_parent_role( $parent_id ); $created = array(); $warnings = array(); $errors = array(); // Suppress new-user emails during import (unless welcome mail is selected) // Note: wp_new_user_notification() is called directly in the loop and // checks the wp_send_new_user_notification_to_user filter internally — that filter // is only set when $send_welcome=false. The removed actions (register_new_user, // edit_user_created_user) do NOT affect wp_new_user_notification() since // it is called directly, not via those hooks. if ( ! $send_welcome ) { add_filter( 'wp_send_new_user_notification_to_user', '__return_false' ); } else { // Log mail failures during import (wp_mail returns false, never throws) add_action( 'wp_mail_failed', 'b2b_importer_log_mail_error' ); } add_filter( 'wp_send_new_user_notification_to_admin', '__return_false' ); remove_action( 'register_new_user', 'wp_send_new_user_notifications' ); remove_action( 'edit_user_created_user', 'wp_send_new_user_notifications', 10 ); // Multisite: suppress "New User Registration" email to network admin if ( is_multisite() ) { remove_action( 'wpmu_new_user', 'newuser_notify_siteadmin', 10 ); } foreach ( $emails as $email ) { $email = sanitize_email( $email ); if ( empty( $email ) || ! is_email( $email ) ) { $errors[] = array( 'email' => $email ?: '(empty line)', 'message' => 'Invalid email address.', ); continue; } $existing_user = get_user_by( 'email', $email ); // ------------------------------------------------------- // User already exists // ------------------------------------------------------- if ( $existing_user ) { if ( $is_multisite && ! is_user_member_of_blog( $existing_user->ID, $current_blog ) ) { $warnings[] = array( 'email' => $email, 'message' => sprintf( 'Exists in the network (ID %d, %s) but not on this site. Add manually via Users → Add Existing, then run the import again.', $existing_user->ID, $existing_user->user_login ), ); } else { $errors[] = array( 'email' => $email, 'message' => sprintf( 'Already registered on this site (ID %d). Skipping.', $existing_user->ID ), ); } continue; } // ------------------------------------------------------- // Brand new user // ------------------------------------------------------- $username = b2b_importer_generate_username( $email ); if ( is_wp_error( $username ) ) { $errors[] = array( 'email' => $email, 'message' => $username->get_error_message(), ); continue; } $password = wp_generate_password( 16, true, false ); if ( $is_multisite ) { $user_id = wpmu_create_user( $username, $password, $email ); if ( is_wp_error( $user_id ) || ! $user_id ) { $msg = is_wp_error( $user_id ) ? $user_id->get_error_message() : 'Unknown error during creation.'; $errors[] = array( 'email' => $email, 'message' => $msg, ); continue; } $added = add_user_to_blog( $current_blog, $user_id, $parent_role ); if ( is_wp_error( $added ) ) { $errors[] = array( 'email' => $email, 'message' => sprintf( 'User was created in the network (ID %d) but could not be added to this site: %s. You can add them manually via Users → Add Existing.', $user_id, $added->get_error_message() ), ); continue; } } else { $user_id = wp_insert_user( array( 'user_login' => $username, 'user_email' => $email, 'user_pass' => $password, 'role' => $parent_role, ) ); if ( is_wp_error( $user_id ) ) { $errors[] = array( 'email' => $email, 'message' => $user_id->get_error_message(), ); continue; } } // Connect as subaccount (inherits group, permissions etc.) b2b_importer_connect_subaccount( $user_id, $parent_id, $permissions ); // Precautionary fallback — b2bking_after_subaccount_created hook is not fired, // so no data is normally copied. Enable only if other plugins copy address data. if ( $clear_billing ) { b2b_importer_clear_billing_shipping( $user_id ); } // Set hide_prices_and_shipping meta if selected // Value '1' = prices hidden, '0' = prices visible (matches existing snippet convention) update_user_meta( $user_id, 'hide_prices_and_shipping', $hide_prices ? '1' : '0' ); // Send welcome email if selected (WP standard "Set your password" email) if ( $send_welcome ) { wp_new_user_notification( $user_id, null, 'user' ); } $created[] = array( 'email' => $email, 'username' => $username, 'user_id' => intval( $user_id ), ); } // Restore mail hooks if ( ! $send_welcome ) { remove_filter( 'wp_send_new_user_notification_to_user', '__return_false' ); } else { remove_action( 'wp_mail_failed', 'b2b_importer_log_mail_error' ); } remove_filter( 'wp_send_new_user_notification_to_admin', '__return_false' ); add_action( 'register_new_user', 'wp_send_new_user_notifications' ); add_action( 'edit_user_created_user', 'wp_send_new_user_notifications', 10, 2 ); // Multisite: restore network admin notification if ( is_multisite() ) { add_action( 'wpmu_new_user', 'newuser_notify_siteadmin', 10 ); } if ( function_exists( 'b2bking' ) && method_exists( b2bking(), 'clear_caches_for_user' ) ) { b2bking()->clear_caches_for_user( $parent_id ); } delete_transient( 'b2bking_customers_list_cache' ); delete_transient( 'b2bking_subaccounts_cache_' . $parent_id ); wp_send_json_success( array( 'created' => $created, 'warnings' => $warnings, 'errors' => $errors, 'summary' => sprintf( '%d subaccounts created, %d warnings, %d errors (of %d emails total).%s', count( $created ), count( $warnings ), count( $errors ), count( $emails ), $send_welcome && count( $created ) > 0 ? sprintf( ' Welcome email sent to %d accounts.', count( $created ) ) : '' ), ) ); } // ============================================================ // 14. ADMIN PAGE – HTML + CSS + JS // ============================================================ function b2b_importer_render_page() { // Check that WooCommerce is active if ( ! function_exists( 'WC' ) || ! WC() ) { echo '

B2BKing Subaccount Importer: WooCommerce must be active.

'; return; } wp_enqueue_style( 'select2', WC()->plugin_url() . '/assets/css/select2.css' ); wp_enqueue_script( 'select2', WC()->plugin_url() . '/assets/js/select2/select2.full.min.js', array( 'jquery' ), null, true ); $nonce = wp_create_nonce( 'b2b_importer_nonce' ); $is_multisite = is_multisite(); $site_name = get_bloginfo( 'name' ); ?>

🏢 B2BKing Subaccount Import Multisite

Import new subaccounts by selecting a parent account and entering email addresses. New accounts are created and linked automatically in B2BKing — role and group are inherited from parent.
Network accounts not found on must be added manually first.

Shows name, email, company and B2BKing group. Subaccounts are filtered out.

0 email addresses

Max emails per run. Separate with line breaks, commas or semicolons.

These permissions are applied to all new subaccounts in this import.

If unchecked, users must log in via "Lost your password?".

Precautionary fallback. Since the b2bking_after_subaccount_created hook is not fired during import, no billing data is normally copied. Enable this only if you suspect other plugins or hooks copy address data.

Sets the hide_prices_and_shipping user meta to 1. Requires the "Hide Prices" code snippet to be active for this to have any effect.

Multisite: Email addresses found in the network but not on this site are marked with a warning. Add them manually via Users → Add Existing before running the import again.