Building secure B2B pricing grids with custom REST API Controllers endpoints and role overrides
Setting Up the Custom REST API Endpoint
To manage B2B pricing grids securely, we’ll leverage WordPress’s REST API. This involves creating a custom endpoint that can be accessed programmatically. We’ll define a new route within the `register_rest_route` function, specifying the namespace, route, and the callback function that will handle requests.
For this example, let’s assume our plugin is named ‘b2b-pricing’ and our endpoint will be `/b2b-pricing/v1/pricing-grid`. The callback function will be responsible for fetching or updating pricing data based on the request parameters.
Registering the REST API Route
This code snippet should be placed within your plugin’s main PHP file or an included file that’s loaded on the admin side.
add_action( 'rest_api_init', function () {
register_rest_route( 'b2b-pricing/v1', '/pricing-grid', array(
'methods' => WP_REST_Server::READABLE, // Or WP_REST_Server::EDITABLE for POST/PUT
'callback' => 'b2b_pricing_get_grid_callback',
'permission_callback' => 'b2b_pricing_permissions_check',
) );
} );
The Callback Function
The `b2b_pricing_get_grid_callback` function will contain the core logic for retrieving pricing data. For simplicity, we’ll simulate fetching data from a custom database table or options. In a real-world scenario, you’d replace this with your actual data retrieval mechanism.
function b2b_pricing_get_grid_callback( WP_REST_Request $request ) {
// In a real application, fetch pricing data from your custom table or options.
// Example: $pricing_data = get_option( 'b2b_pricing_grid_data' );
$pricing_data = array(
'product_a' => array(
'tier_1' => 10.00,
'tier_2' => 9.50,
'tier_3' => 9.00,
),
'product_b' => array(
'tier_1' => 25.00,
'tier_2' => 23.75,
'tier_3' => 22.50,
),
);
// You might want to filter data based on user roles or other criteria here.
// For now, we return all data.
return new WP_REST_Response( $pricing_data, 200 );
}
Implementing Role-Based Access Control
Security is paramount. We need to ensure that only authorized users can access and manipulate pricing data. WordPress’s role and capability system is the ideal mechanism for this. We’ll implement a `permission_callback` for our REST API endpoint.
The Permission Callback Function
The `b2b_pricing_permissions_check` function will be executed before the main callback. It receives the `WP_REST_Request` object and should return `true` if the user has permission, or a `WP_Error` object otherwise.
function b2b_pricing_permissions_check( WP_REST_Request $request ) {
// Define the capability required to access this endpoint.
// For example, 'manage_options' is typically reserved for administrators.
// You might create a custom capability like 'manage_b2b_pricing'.
$required_capability = 'manage_b2b_pricing';
// Check if the current user has the required capability.
if ( ! current_user_can( $required_capability ) ) {
return new WP_Error( 'rest_forbidden', esc_html__( 'You do not have permission to access this resource.', 'b2b-pricing' ), array( 'status' => rest_authorization_required_code() ) );
}
// If the user has the capability, allow access.
return true;
}
Defining Custom Capabilities
To implement granular control, it’s best practice to define custom capabilities. This prevents conflicts with other plugins and makes your permission logic clearer. Add this to your plugin’s activation hook.
function b2b_pricing_add_custom_capabilities() {
// Get the administrator role.
$role = get_role( 'administrator' );
// Add the custom capability if it doesn't exist.
if ( $role && ! $role->has_cap( 'manage_b2b_pricing' ) ) {
$role->add_cap( 'manage_b2b_pricing' );
}
// You can also add this capability to other roles as needed.
// For example, a custom 'B2B Manager' role.
// $b2b_manager_role = get_role( 'b2b_manager' );
// if ( $b2b_manager_role && ! $b2b_manager_role->has_cap( 'manage_b2b_pricing' ) ) {
// $b2b_manager_role->add_cap( 'manage_b2b_pricing' );
// }
}
register_activation_hook( __FILE__, 'b2b_pricing_add_custom_capabilities' );
Overriding Roles for Specific Pricing Tiers
A common B2B requirement is to show different pricing tiers to different customer segments, often mapped to user roles or custom user meta. We can extend our REST API endpoint to handle this by checking user meta or specific role assignments.
Modifying the Callback for Tiered Pricing
We’ll modify the `b2b_pricing_get_grid_callback` to inspect the current user’s roles or custom meta and return only the relevant pricing tier.
function b2b_pricing_get_grid_callback( WP_REST_Request $request ) {
$current_user = wp_get_current_user();
$pricing_data = array();
// Define your pricing tiers and their associated roles/meta.
// This is a simplified example. You'd likely store this in options or a custom table.
$all_pricing_tiers = array(
'base' => array( // Default pricing for everyone or unassigned users
'product_a' => array( 'price' => 12.00 ),
'product_b' => array( 'price' => 30.00 ),
),
'wholesale' => array(
'product_a' => array( 'price' => 10.00 ),
'product_b' => array( 'price' => 25.00 ),
),
'distributor' => array(
'product_a' => array( 'price' => 9.00 ),
'product_b' => array( 'price' => 22.50 ),
),
);
$user_tier = 'base'; // Default tier
// Check for specific roles that map to pricing tiers.
if ( user_can( $current_user->ID, 'wholesale_customer' ) ) {
$user_tier = 'wholesale';
} elseif ( user_can( $current_user->ID, 'distributor_customer' ) ) {
$user_tier = 'distributor';
}
// You could also check custom user meta:
// $customer_level = get_user_meta( $current_user->ID, 'customer_level', true );
// if ( $customer_level === 'wholesale' ) {
// $user_tier = 'wholesale';
// }
// Assign the determined tier's pricing.
if ( isset( $all_pricing_tiers[ $user_tier ] ) ) {
$pricing_data = $all_pricing_tiers[ $user_tier ];
} else {
// Fallback to base pricing if tier is not found.
$pricing_data = $all_pricing_tiers['base'];
}
return new WP_REST_Response( $pricing_data, 200 );
}
Creating Custom Roles for Tiers
To use role-based checks like `user_can( $current_user->ID, ‘wholesale_customer’ )`, you need to define these roles and assign them to users. This can also be done via your plugin’s activation hook or a dedicated setup function.
function b2b_pricing_add_custom_roles() {
// Add 'Wholesale Customer' role
add_role( 'wholesale_customer', __( 'Wholesale Customer' ), array(
'read' => true, // Basic capability
// Add any other capabilities specific to wholesale customers if needed
) );
// Add 'Distributor Customer' role
add_role( 'distributor_customer', __( 'Distributor Customer' ), array(
'read' => true,
// Add any other capabilities specific to distributor customers if needed
) );
}
register_activation_hook( __FILE__, 'b2b_pricing_add_custom_roles' );
Securing the Endpoint with Authentication
By default, WordPress REST API endpoints are accessible to authenticated users. However, for B2B applications, you might need more robust authentication, especially if the API is consumed by external systems. WordPress REST API uses nonces for authenticated requests. When making requests from the frontend JavaScript, you’ll need to include a nonce.
Generating and Using Nonces
In your WordPress theme or plugin, you can pass a nonce to your JavaScript. This nonce is then included in the `X-WP-Nonce` header of your AJAX requests.
First, in your PHP, localize a script to pass the nonce:
function b2b_pricing_enqueue_scripts() {
// Assuming you have a script registered for your frontend
wp_enqueue_script( 'b2b-pricing-frontend', plugin_dir_url( __FILE__ ) . 'js/frontend.js', array( 'jquery' ), '1.0', true );
// Localize the script with the REST API URL and nonce
wp_localize_script( 'b2b-pricing-frontend', 'b2bPricingApi', array(
'root' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ), // The nonce action for REST API
) );
}
add_action( 'wp_enqueue_scripts', 'b2b_pricing_enqueue_scripts' );
Then, in your JavaScript (js/frontend.js), make the AJAX request:
jQuery(document).ready(function($) {
var apiRoot = b2bPricingApi.root;
var nonce = b2bPricingApi.nonce;
var endpoint = 'b2b-pricing/v1/pricing-grid';
$.ajax({
url: apiRoot + endpoint,
method: 'GET',
beforeSend: function ( xhr ) {
xhr.setRequestHeader( 'X-WP-Nonce', nonce );
}
})
.done(function(response) {
console.log('Pricing data:', response);
// Process the pricing data here
})
.fail(function(jqXHR, textStatus, errorThrown) {
console.error('Error fetching pricing data:', textStatus, errorThrown);
if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
console.error('API Error Message:', jqXHR.responseJSON.message);
}
});
});
Handling Updates and Data Persistence
For a complete B2B pricing grid solution, you’ll need to handle updates. This involves creating a new REST API endpoint that accepts `POST` or `PUT` requests. The permission callback remains crucial here to ensure only authorized users can modify pricing.
Creating an Update Endpoint
We’ll register a new route for updating the pricing grid. This route will require a `WP_REST_Server::EDITABLE` method.
add_action( 'rest_api_init', function () {
register_rest_route( 'b2b-pricing/v1', '/pricing-grid', array(
'methods' => WP_REST_Server::EDITABLE, // For POST, PUT, DELETE
'callback' => 'b2b_pricing_update_grid_callback',
'permission_callback' => 'b2b_pricing_permissions_check', // Reuse the same permission check
'args' => array( // Define expected arguments for validation
'pricing_data' => array(
'required' => true,
'type' => 'array',
'sanitize_callback' => 'wp_slash_deep', // Sanitize nested arrays
'validate_callback' => function($param, $request, $key) {
// Add more specific validation for the structure of pricing_data if needed
return is_array($param);
}
),
),
) );
} );
The Update Callback Function
This callback will receive the new pricing data, validate it, and save it to your persistent storage (e.g., `wp_options` or a custom database table).
function b2b_pricing_update_grid_callback( WP_REST_Request $request ) {
$pricing_data = $request->get_param( 'pricing_data' );
// Basic validation: Ensure pricing_data is an array.
if ( ! is_array( $pricing_data ) ) {
return new WP_Error( 'rest_invalid_param', esc_html__( 'Invalid pricing data format.', 'b2b-pricing' ), array( 'status' => 400 ) );
}
// Further validation: Check the structure of the pricing data.
// For example, ensure each product has a price.
foreach ( $pricing_data as $product_key => $tiers ) {
if ( ! is_array( $tiers ) ) {
return new WP_Error( 'rest_invalid_param', sprintf( esc_html__( 'Invalid tier data for product %s.', 'b2b-pricing' ), $product_key ), array( 'status' => 400 ) );
}
foreach ( $tiers as $tier_key => $price_info ) {
if ( ! isset( $price_info['price'] ) || ! is_numeric( $price_info['price'] ) ) {
return new WP_Error( 'rest_invalid_param', sprintf( esc_html__( 'Invalid price for product %s, tier %s.', 'b2b-pricing' ), $product_key, $tier_key ), array( 'status' => 400 ) );
}
}
}
// Save the validated pricing data.
// Example: update_option( 'b2b_pricing_grid_data', $pricing_data );
// In a real scenario, you might save this to a custom table.
$saved = update_option( 'b2b_pricing_grid_data', $pricing_data );
if ( $saved ) {
return new WP_REST_Response( array( 'message' => esc_html__( 'Pricing grid updated successfully.', 'b2b-pricing' ) ), 200 );
} else {
return new WP_Error( 'rest_server_error', esc_html__( 'Failed to update pricing grid.', 'b2b-pricing' ), array( 'status' => 500 ) );
}
}
Conclusion
By implementing custom REST API endpoints with robust permission callbacks and leveraging WordPress’s role management system, you can build secure and flexible B2B pricing grids. This approach allows for programmatic access, granular control over who sees what pricing, and a clear path for managing complex pricing structures within your WordPress ecosystem.