Building secure B2B pricing grids with custom WordPress Settings API endpoints and role overrides
Leveraging WordPress Settings API for Secure B2B Pricing Grids
Enterprise-grade B2B pricing often necessitates dynamic, role-specific price adjustments that go beyond standard WooCommerce product variations. Implementing such a system within WordPress requires a robust, secure, and extensible architecture. This post details how to build a custom pricing grid solution using the WordPress Settings API, coupled with granular role-based access control, to manage and display bespoke pricing for different client tiers.
Designing the Settings Structure with Settings API
The WordPress Settings API provides a structured way to register settings, sections, and fields, ensuring data is properly saved, sanitized, and displayed. For a B2B pricing grid, we’ll define a custom setting group to hold our pricing data. This data will likely be a complex array, mapping product IDs or SKUs to different pricing tiers, which in turn are associated with specific user roles.
We’ll use a single option to store the entire pricing grid configuration. This simplifies management and reduces database overhead compared to individual options for each product/role combination. The structure will be an associative array where keys represent product identifiers and values are arrays of role-specific prices.
Registering the Custom Setting
The core of this implementation lies in registering a custom option group and its associated fields. We’ll hook into the `admin_init` action to perform these registrations.
add_action( 'admin_init', 'b2b_pricing_register_settings' );
function b2b_pricing_register_settings() {
// Register a new setting for our pricing grid data.
// 'b2b_pricing_grid_data' is the option name.
// 'b2b_pricing_sanitize_grid_data' is the sanitization callback.
register_setting(
'b2b_pricing_options_group', // Option group
'b2b_pricing_grid_data', // Option name
'b2b_pricing_sanitize_grid_data' // Sanitization callback
);
// Add a new section to the settings page.
add_settings_section(
'b2b_pricing_section', // Section ID
__( 'B2B Pricing Grid Configuration', 'b2b-pricing' ), // Section title
'b2b_pricing_section_callback', // Callback function to display section description
'b2b_pricing_settings' // Page slug where this section will appear
);
// Add a field for the pricing grid.
// This field will contain a textarea for JSON input or a more complex UI.
add_settings_field(
'b2b_pricing_grid_field', // Field ID
__( 'Pricing Grid Data (JSON)', 'b2b-pricing' ), // Field title
'b2b_pricing_grid_field_callback', // Callback function to render the field
'b2b_pricing_settings', // Page slug
'b2b_pricing_section' // Section ID
);
}
// Callback function for the section description.
function b2b_pricing_section_callback() {
echo '' . __( 'Enter your B2B pricing data in JSON format. The structure should map product IDs to role-specific prices.', 'b2b-pricing' ) . '
';
echo '' . __( 'Example structure: {"123": {"wholesale_customer": 10.50, "distributor": 8.75}, "456": {"wholesale_customer": 25.00}}', 'b2b-pricing' ) . '
';
}
// Callback function to render the pricing grid field.
function b2b_pricing_grid_field_callback() {
$pricing_data = get_option( 'b2b_pricing_grid_data', array() );
$json_data = json_encode( $pricing_data, JSON_PRETTY_PRINT );
?>
$role_prices ) {
if ( ! is_numeric( $product_id ) ) {
continue; // Skip if product ID is not numeric
}
$sanitized_role_prices = array();
if ( is_array( $role_prices ) ) {
foreach ( $role_prices as $role_slug => $price ) {
// Ensure role slug is a string and price is a valid float.
if ( is_string( $role_slug ) && is_numeric( $price ) ) {
$sanitized_role_prices[sanitize_key( $role_slug )] = (float) $price;
}
}
}
if ( ! empty( $sanitized_role_prices ) ) {
$sanitized_data[$product_id] = $sanitized_role_prices;
}
}
return $sanitized_data;
}
Creating the Settings Page
To make the settings accessible, we need to register a menu page. This is typically done using the `admin_menu` action.
add_action( 'admin_menu', 'b2b_pricing_add_admin_menu' );
function b2b_pricing_add_admin_menu() {
add_options_page(
__( 'B2B Pricing Settings', 'b2b-pricing' ), // Page title
__( 'B2B Pricing', 'b2b-pricing' ), // Menu title
'manage_options', // Capability required to access
'b2b_pricing_settings', // Menu slug
'b2b_pricing_options_page_html' // Callback function to render the page content
);
}
function b2b_pricing_options_page_html() {
// Check user capabilities
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
Implementing Role-Based Price Overrides
Once the pricing data is stored, we need a mechanism to retrieve and apply these custom prices based on the currently logged-in user's role. This involves hooking into WooCommerce's price display and cart/checkout processes.
Retrieving Custom Prices
A helper function can abstract the logic for fetching the correct price for a given product and user role.
/**
* Get the custom price for a product based on the current user's role.
*
* @param int $product_id The ID of the product.
* @return float|null The custom price if found, otherwise null.
*/
function b2b_pricing_get_custom_price( $product_id ) {
if ( ! is_user_logged_in() ) {
return null;
}
$pricing_data = get_option( 'b2b_pricing_grid_data', array() );
if ( empty( $pricing_data ) || ! isset( $pricing_data[$product_id] ) ) {
return null;
}
$user = wp_get_current_user();
$user_roles = (array) $user->roles;
// Prioritize roles if a specific order is needed, or simply iterate.
// For simplicity, we'll take the first matching role found.
foreach ( $user_roles as $role ) {
if ( isset( $pricing_data[$product_id][$role] ) ) {
return (float) $pricing_data[$product_id][$role];
}
}
return null; // No custom price found for any of the user's roles
}
Applying Prices to WooCommerce Products
We can use WooCommerce hooks to override the default prices. The `woocommerce_product_get_price`, `woocommerce_product_get_regular_price`, and `woocommerce_product_get_sale_price` filters are prime candidates.
// Override the regular price
add_filter( 'woocommerce_product_get_regular_price', 'b2b_pricing_override_product_price', 10, 2 );
// Override the sale price (if applicable)
add_filter( 'woocommerce_product_get_sale_price', 'b2b_pricing_override_product_price', 10, 2 );
// Override the displayed price (for single product pages, archives, etc.)
add_filter( 'woocommerce_product_get_price', 'b2b_pricing_override_product_price', 10, 2 );
function b2b_pricing_override_product_price( $price, $product ) {
if ( ! $product instanceof WC_Product ) {
return $price;
}
$product_id = $product->get_id();
$custom_price = b2b_pricing_get_custom_price( $product_id );
if ( $custom_price !== null ) {
// Ensure we are returning a string as expected by WooCommerce filters
return (string) $custom_price;
}
return $price; // Return original price if no custom price is found
}
// Also handle variations if applicable
add_filter( 'woocommerce_product_variation_get_regular_price', 'b2b_pricing_override_variation_price', 10, 2 );
add_filter( 'woocommerce_product_variation_get_sale_price', 'b2b_pricing_override_variation_price', 10, 2 );
add_filter( 'woocommerce_product_variation_get_price', 'b2b_pricing_override_variation_price', 10, 2 );
function b2b_pricing_override_variation_price( $price, $variation ) {
if ( ! $variation instanceof WC_Product_Variation ) {
return $price;
}
$product_id = $variation->get_parent_id(); // Get parent product ID for variation
$custom_price = b2b_pricing_get_custom_price( $product_id );
// For variations, we might want to check if the variation itself has a role-specific price,
// or if the parent product's role-specific price should apply to all variations.
// This example assumes the parent product's role price applies to all variations.
// A more complex system might involve mapping variation IDs to pricing data.
if ( $custom_price !== null ) {
return (string) $custom_price;
}
return $price;
}
Securing Access to Pricing Settings
The `add_options_page` function already includes a `manage_options` capability check, which is typically reserved for Administrators. For more granular control, you could:
- Create a custom user role (e.g., "Pricing Manager") with specific capabilities.
- Modify the `add_options_page` capability argument to target this custom role.
- Implement checks within the `b2b_pricing_get_custom_price` function to ensure only authorized users can *see* B2B pricing, even if they aren't administrators managing the settings. This might involve checking for specific roles or custom meta on the user object.
For instance, to restrict viewing B2B prices to users with the 'wholesale_customer' role:
function b2b_pricing_get_custom_price( $product_id ) {
// Restrict access to B2B pricing display to logged-in users with specific roles
if ( ! is_user_logged_in() ) {
return null;
}
$user = wp_get_current_user();
$allowed_roles_for_viewing = array( 'wholesale_customer', 'distributor', 'administrator' ); // Example roles
$has_view_access = false;
foreach ( $user->roles as $role ) {
if ( in_array( $role, $allowed_roles_for_viewing ) ) {
$has_view_access = true;
break;
}
}
if ( ! $has_view_access ) {
return null; // User cannot see B2B prices
}
$pricing_data = get_option( 'b2b_pricing_grid_data', array() );
if ( empty( $pricing_data ) || ! isset( $pricing_data[$product_id] ) ) {
return null;
}
// Now, find the specific price for the user's role
$user_roles = (array) $user->roles;
foreach ( $user_roles as $role ) {
if ( isset( $pricing_data[$product_id][$role] ) ) {
return (float) $pricing_data[$product_id][$role];
}
}
return null;
}
Advanced Considerations and Enhancements
This foundational implementation can be extended significantly:
- UI Improvements: Replace the JSON textarea with a more user-friendly interface using JavaScript (e.g., React, Vue) to dynamically add/edit products and role prices. This would involve custom AJAX endpoints for saving data.
- Product Identification: Instead of relying solely on numeric product IDs, consider using SKUs or custom product meta fields for more robust identification, especially in complex catalogs.
- Role Prioritization: If users can have multiple roles, implement a priority system for which role's price takes precedence.
- Tiered Pricing: Extend the data structure to support quantity-based tiered pricing within each role.
- Caching: For performance, implement transient caching for pricing data, especially if the pricing grid is large and frequently accessed.
- Order Processing: Ensure that prices applied during checkout are correctly reflected in order totals and line items. WooCommerce hooks like `woocommerce_checkout_create_order_line_item` might be necessary.
- API Access: For headless implementations or integrations, expose this pricing logic via a REST API endpoint, ensuring proper authentication and authorization.
By combining the WordPress Settings API with WooCommerce hooks and robust role management, you can construct a powerful and secure system for managing complex B2B pricing structures, tailored to the specific needs of enterprise clients.