The requirement, and the usual answer
A store sells to the public and to trade customers. Trade customers should see lower prices, perhaps in tiers by quantity, perhaps excluding tax where retail includes it, perhaps with a minimum order. The usual answer is a wholesale plugin: capable, licensed annually, several thousand lines of code, adding its own tables, admin screens and front-end assets to every page load.
For many stores that plugin is right. For a store whose wholesale rules are simple, a hundred lines in the site plugin do the job with no licence, no bloat and no plugin to break on the next update. Here is what that looks like, and where its limits are.
Step 1: a wholesale role
add_action( 'init', function () {
if ( ! get_role( 'wholesale_customer' ) ) {
add_role( 'wholesale_customer', 'Wholesale customer', get_role( 'customer' )->capabilities );
}
} );
function acme_is_wholesale( $user_id = null ) {
$user = $user_id ? get_user_by( 'id', $user_id ) : wp_get_current_user();
return $user && in_array( 'wholesale_customer', (array) $user->roles, true );
}Approve trade accounts by changing the user's role in the admin. A registration form with an "apply for a trade account" checkbox that creates the user as a normal customer and emails you to approve them is a few more lines or a form plugin.
Step 2: a wholesale price per product
Store the wholesale price as product meta. A field on the product edit screen via ACF, or a small custom field:
add_action( 'woocommerce_product_options_pricing', function () {
woocommerce_wp_text_input( [
'id' => '_wholesale_price',
'label' => 'Wholesale price (' . get_woocommerce_currency_symbol() . ')',
'data_type' => 'price',
] );
} );
add_action( 'woocommerce_process_product_meta', function ( $post_id ) {
if ( isset( $_POST['_wholesale_price'] ) ) {
update_post_meta( $post_id, '_wholesale_price', wc_format_decimal( wp_unslash( $_POST['_wholesale_price'] ) ) );
}
} );Repeat for variations with the woocommerce_variation_options_pricing and woocommerce_save_product_variation hooks.
Step 3: apply it
Filter the price WooCommerce uses whenever a wholesale user is looking or buying:
function acme_wholesale_price( $price, $product ) {
if ( is_admin() && ! wp_doing_ajax() ) return $price;
if ( ! acme_is_wholesale() ) return $price;
$w = $product->get_meta( '_wholesale_price' );
return ( '' !== $w && null !== $w ) ? $w : $price;
}
add_filter( 'woocommerce_product_get_price', 'acme_wholesale_price', 20, 2 );
add_filter( 'woocommerce_product_get_regular_price', 'acme_wholesale_price', 20, 2 );
add_filter( 'woocommerce_product_variation_get_price', 'acme_wholesale_price', 20, 2 );
add_filter( 'woocommerce_product_variation_get_regular_price', 'acme_wholesale_price', 20, 2 );
add_filter( 'woocommerce_product_get_sale_price', function ( $price, $product ) {
return acme_is_wholesale() ? '' : $price; // no retail sales for wholesale
}, 20, 2 );Variable product price ranges use cached hashes; filter woocommerce_get_variation_prices_hash to include the role so wholesale and retail users do not share a cached range:
add_filter( 'woocommerce_get_variation_prices_hash', function ( $hash ) {
$hash[] = acme_is_wholesale() ? 'wholesale' : 'retail';
return $hash;
} );Step 4: quantity tiers, if needed
Percentage discounts at quantity breaks, applied in the cart:
add_action( 'woocommerce_before_calculate_totals', function ( $cart ) {
if ( ! acme_is_wholesale() || did_action( 'woocommerce_before_calculate_totals' ) > 1 ) return;
$tiers = [ 50 => 0.10, 20 => 0.05 ]; // qty => discount
foreach ( $cart->get_cart() as $item ) {
$base = (float) $item['data']->get_price();
foreach ( $tiers as $qty => $off ) {
if ( $item['quantity'] >= $qty ) { $item['data']->set_price( $base * ( 1 - $off ) ); break; }
}
}
} );Show the tiers on the product page with a small table so trade buyers know they exist.
Step 5: tax display by role
Retail customers see prices including tax; trade customers expect excluding. WooCommerce's display settings are global, but filterable:
add_filter( 'option_woocommerce_tax_display_shop', fn( $v ) => acme_is_wholesale() ? 'excl' : $v );
add_filter( 'option_woocommerce_tax_display_cart', fn( $v ) => acme_is_wholesale() ? 'excl' : $v );Tax is still calculated and charged correctly; only the display changes. For genuinely tax-exempt trade customers (with a valid number), set $customer->set_is_vat_exempt( true ) on login for the role.
Step 6: hide prices from guests, optionally
Stores that do not want the public to see wholesale exist can hide prices and the add-to-cart button for logged-out visitors, or for non-wholesale users on wholesale-only products:
add_filter( 'woocommerce_get_price_html', function ( $html, $product ) {
if ( ! is_user_logged_in() && $product->get_meta( '_wholesale_only' ) ) {
return '<a href="' . esc_url( wc_get_page_permalink( 'myaccount' ) ) . '">Log in for trade pricing</a>';
}
return $html;
}, 20, 2 );
add_filter( 'woocommerce_is_purchasable', function ( $ok, $product ) {
return ( ! is_user_logged_in() && $product->get_meta( '_wholesale_only' ) ) ? false : $ok;
}, 20, 2 );Step 7: minimum order and payment terms
A minimum wholesale order total is a check in woocommerce_check_cart_items adding an error notice. Payment on account (invoice, net 30) is a gateway: enable "Cheque" or "BACS" renamed, restricted to the wholesale role with a woocommerce_available_payment_gateways filter.
Caching
Page caches serve the same HTML to everyone. Wholesale prices in the page mean logged-in wholesale users must bypass the cache (most caches bypass logged-in users by default; confirm) and price fragments loaded by AJAX must key on role. If the store serves prices via the Store API or blocks, the API responses respect the filters above but any CDN caching of /wc/store/ must be off.
When to use the plugin instead
The code above covers: one wholesale role, one wholesale price per product or variation, simple quantity tiers, tax display, hidden prices, a minimum order and an account gateway. Reach for a plugin (Wholesale Suite, B2BKing, Wholesale for WooCommerce) when you need several wholesale tiers with different prices each, per-customer price lists, category-wide percentage rules managed in the admin by non-developers, a quote-request flow, or an approval workflow with its own UI. Those are real requirements and building them is more than a hundred lines.
Where this sits
Whether a store's wholesale rules fit in the site plugin or need a product is one of the first scoping questions in WooCommerce work, and the answer is often the small version, because the rules were simple all along and the plugin was bought before anyone asked.
