Building secure B2B pricing grids with custom Metadata API (add_post_meta) endpoints and role overrides
Securing B2B Pricing Grids with Custom WordPress Metadata Endpoints
Building dynamic, role-specific pricing grids in WordPress for B2B clients presents a unique set of challenges, particularly around data security and granular access control. Standard WordPress user roles and capabilities often fall short when dealing with complex pricing tiers, volume discounts, and client-specific negotiated rates. This post details a robust solution leveraging custom WordPress REST API endpoints for managing post meta, combined with advanced role overrides, to create a secure and flexible B2B pricing system.
Designing the Metadata Schema for Pricing
The foundation of our B2B pricing grid lies in how we structure the pricing data. We’ll use custom post meta associated with a custom post type (e.g., ‘Product’ or ‘Service’) to store this information. For a B2B context, pricing isn’t monolithic. It often varies based on customer tier, volume, or specific contract terms. We’ll design our meta keys to reflect this complexity.
Consider a product with the following pricing dimensions:
- Base Price (for general public/retail)
- Tier 1 Price (e.g., for Bronze partners)
- Tier 2 Price (e.g., for Silver partners)
- Tier 3 Price (e.g., for Gold partners)
- Volume Discount Threshold 1 (e.g., 100 units)
- Volume Discount Rate 1 (e.g., 5% off Tier 3)
- Volume Discount Threshold 2 (e.g., 500 units)
- Volume Discount Rate 2 (e.g., 10% off Tier 3)
- Contracted Price (for specific clients, overriding all others)
These would translate into meta keys like:
_base_price_tier_1_price_tier_2_price_tier_3_price_volume_discount_threshold_1_volume_discount_rate_1_volume_discount_threshold_2_volume_discount_rate_2_contracted_price_{client_id}(where{client_id}is a unique identifier for the client)
Implementing Custom REST API Endpoints for Metadata
WordPress’s built-in REST API provides endpoints for posts and their meta. However, for B2B pricing, we need more control. We’ll create custom endpoints to manage these specific pricing meta fields, ensuring that only authorized users can read or write them. This involves hooking into the rest_api_init action.
Let’s define an endpoint to retrieve pricing data for a specific product, allowing filtering by user role or client ID.
Endpoint for Retrieving Pricing Data
We’ll register a route under /myplugin/v1/pricing/product/{id}. This endpoint will fetch the product’s meta data and intelligently return the appropriate price based on the authenticated user’s role or associated client data.
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/pricing/product/(?P<id>\d+)', array(
'methods' => 'GET',
'callback' => 'myplugin_get_product_pricing',
'permission_callback' => '__return_true', // We'll handle permissions within the callback
) );
} );
function myplugin_get_product_pricing( WP_REST_Request $request ) {
$product_id = $request['id'];
$user = wp_get_current_user();
$pricing_data = array();
// Basic product existence check
$post = get_post( $product_id );
if ( ! $post || $post->post_type !== 'product' ) { // Assuming 'product' is your CPT
return new WP_Error( 'rest_not_found', 'Product not found', array( 'status' => 404 ) );
}
// Fetch all relevant pricing meta
$meta_keys = array(
'_base_price',
'_tier_1_price',
'_tier_2_price',
'_tier_3_price',
'_volume_discount_threshold_1',
'_volume_discount_rate_1',
'_volume_discount_threshold_2',
'_volume_discount_rate_2',
);
foreach ( $meta_keys as $key ) {
$pricing_data[$key] = get_post_meta( $product_id, $key, true );
}
// --- Role-based pricing logic ---
$user_roles = (array) $user->roles;
$effective_price = $pricing_data['_base_price']; // Default to base price
if ( in_array( 'b2b_tier_3', $user_roles ) ) {
$effective_price = $pricing_data['_tier_3_price'] ?: $effective_price;
} elseif ( in_array( 'b2b_tier_2', $user_roles ) ) {
$effective_price = $pricing_data['_tier_2_price'] ?: $effective_price;
} elseif ( in_array( 'b2b_tier_1', $user_roles ) ) {
$effective_price = $pricing_data['_tier_1_price'] ?: $effective_price;
}
// --- Contracted pricing logic ---
// This assumes you have a way to map the current user to a client ID.
// For simplicity, let's assume a meta field on the user object: 'client_id'
$client_id = get_user_meta( $user->ID, 'client_id', true );
if ( ! empty( $client_id ) ) {
$contracted_price_key = '_contracted_price_' . sanitize_key( $client_id );
$contracted_price = get_post_meta( $product_id, $contracted_price_key, true );
if ( ! empty( $contracted_price ) ) {
$effective_price = $contracted_price;
}
}
// --- Volume discount application (example for Tier 3 or contracted price) ---
// This logic would typically be applied client-side or in a more complex backend process
// For this example, we'll just return the base pricing structure.
// A full implementation would involve passing quantity to the API or handling it in the frontend.
$response_data = array(
'product_id' => $product_id,
'base_price' => $pricing_data['_base_price'],
'tier_1_price' => $pricing_data['_tier_1_price'],
'tier_2_price' => $pricing_data['_tier_2_price'],
'tier_3_price' => $pricing_data['_tier_3_price'],
'contracted_price' => isset( $contracted_price ) ? $contracted_price : null,
'effective_price' => $effective_price, // The price determined by role/contract
'volume_discounts' => array(
'threshold_1' => $pricing_data['_volume_discount_threshold_1'],
'rate_1' => $pricing_data['_volume_discount_rate_1'],
'threshold_2' => $pricing_data['_volume_discount_threshold_2'],
'rate_2' => $pricing_data['_volume_discount_rate_2'],
),
'user_roles' => $user_roles,
'client_id' => $client_id,
);
$response = new WP_REST_Response( $response_data );
$response->set_status( 200 );
return $response;
}
Endpoint for Updating Pricing Data
Updating pricing data requires strict authorization. We’ll create a POST endpoint /myplugin/v1/pricing/product/{id}/update. This endpoint will only be accessible to users with a specific capability, such as manage_b2b_pricing.
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/pricing/product/(?P<id>\d+)/update', array(
'methods' => 'POST',
'callback' => 'myplugin_update_product_pricing',
'permission_callback' => 'myplugin_update_pricing_permissions_check',
'args' => array( // Define expected arguments for validation
'base_price' => array(
'required' => false,
'type' => 'string', // Use string for potential currency symbols, or float/number
'sanitize_callback' => 'sanitize_text_field',
),
'_tier_1_price' => array(
'required' => false,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
// ... other price fields ...
'_contracted_prices' => array( // Expect an array of { client_id: price }
'required' => false,
'type' => 'array',
'sanitize_callback' => array( $this, 'sanitize_contracted_prices' ), // Custom sanitizer
),
),
) );
} );
function myplugin_update_pricing_permissions_check( WP_REST_Request $request ) {
// Check if the current user has the capability to manage B2B pricing
if ( ! current_user_can( 'manage_b2b_pricing' ) ) {
return new WP_Error( 'rest_forbidden', esc_html__( 'You do not have permission to update pricing.', 'myplugin' ), array( 'status' => 403 ) );
}
return true;
}
function myplugin_update_product_pricing( WP_REST_Request $request ) {
$product_id = $request['id'];
$params = $request->get_params();
// Basic product existence check
$post = get_post( $product_id );
if ( ! $post || $post->post_type !== 'product' ) {
return new WP_Error( 'rest_not_found', 'Product not found', array( 'status' => 404 ) );
}
// Update standard meta fields
$meta_to_update = array(
'_base_price',
'_tier_1_price',
'_tier_2_price',
'_tier_3_price',
'_volume_discount_threshold_1',
'_volume_discount_rate_1',
'_volume_discount_threshold_2',
'_volume_discount_rate_2',
);
foreach ( $meta_to_update as $key ) {
if ( isset( $params[$key] ) ) {
update_post_meta( $product_id, $key, $params[$key] );
}
}
// Handle contracted prices separately
if ( isset( $params['_contracted_prices'] ) && is_array( $params['_contracted_prices'] ) ) {
// First, remove any existing contracted prices for this product to ensure clean updates
// This requires a more complex query or a known set of client IDs.
// For simplicity, let's assume we're only ADDING/OVERWRITING.
// A more robust solution would involve fetching existing contracted prices and diffing.
foreach ( $params['_contracted_prices'] as $client_id => $price ) {
if ( ! empty( $client_id ) && ! empty( $price ) ) {
$contracted_price_key = '_contracted_price_' . sanitize_key( $client_id );
update_post_meta( $product_id, $contracted_price_key, sanitize_text_field( $price ) );
}
}
}
return new WP_REST_Response( array( 'success' => true, 'message' => 'Pricing updated successfully.' ), 200 );
}
// Custom sanitizer for contracted prices
function sanitize_contracted_prices( $value, $request, $param ) {
if ( ! is_array( $value ) ) {
return new WP_Error( 'rest_invalid_param', esc_html__( 'Contracted prices must be an array.', 'myplugin' ), array( 'status' => 400 ) );
}
$sanitized = array();
foreach ( $value as $client_id => $price ) {
$sanitized_client_id = sanitize_key( $client_id );
if ( ! empty( $sanitized_client_id ) ) {
$sanitized[$sanitized_client_id] = sanitize_text_field( $price );
}
}
return $sanitized;
}
Implementing Role Overrides and Capabilities
To manage access effectively, we need custom user roles and capabilities. This ensures that only designated personnel can view or modify B2B pricing information.
Defining Custom Roles and Capabilities
We’ll use the add_role() function, typically hooked into plugin activation, to create roles like ‘B2B Manager’ and ‘B2B Sales Rep’. These roles will be granted specific capabilities.
// Hook into plugin activation
register_activation_hook( __FILE__, 'myplugin_activate' );
function myplugin_activate() {
// Add B2B Manager role with pricing management capabilities
add_role(
'b2b_manager',
__( 'B2B Manager', 'myplugin' ),
array(
'read' => true, // Basic read access
'edit_posts' => true, // Can edit products/services
'upload_files' => true,
'manage_b2b_pricing' => true, // Custom capability to manage B2B pricing
'read_private_products' => true, // If products are private
)
);
// Add B2B Sales Rep role with pricing viewing capabilities
add_role(
'b2b_sales_rep',
__( 'B2B Sales Rep', 'myplugin' ),
array(
'read' => true,
'edit_posts' => false, // Cannot edit products
'manage_b2b_pricing' => false, // Cannot manage pricing
'read_private_products' => true,
)
);
// Add B2B Tiers roles for pricing tiers
add_role(
'b2b_tier_1',
__( 'B2B Tier 1 Client', 'myplugin' ),
array( 'read' => true )
);
add_role(
'b2b_tier_2',
__( 'B2B Tier 2 Client', 'myplugin' ),
array( 'read' => true )
);
add_role(
'b2b_tier_3',
__( 'B2B Tier 3 Client', 'myplugin' ),
array( 'read' => true )
);
}
// Hook into plugin deactivation to remove roles
register_deactivation_hook( __FILE__, 'myplugin_deactivate' );
function myplugin_deactivate() {
remove_role( 'b2b_manager' );
remove_role( 'b2b_sales_rep' );
remove_role( 'b2b_tier_1' );
remove_role( 'b2b_tier_2' );
remove_role( 'b2b_tier_3' );
}
The manage_b2b_pricing capability is crucial. It’s checked in our permission_callback for the update endpoint. For the GET endpoint, we initially set 'permission_callback' => '__return_true', but we implement the actual access control logic within the callback function itself, allowing for more dynamic checks (e.g., checking if the user is logged in, or if they are a specific client type).
Client-Specific Pricing and User Mapping
For true B2B functionality, we need to associate users with specific client accounts, which then dictates their contracted pricing. This can be achieved by:
- Adding a
client_idmeta field to the user profile. - Creating a custom mapping table or using a plugin like “ACF Extended” or “Meta Box” to manage user-client relationships.
- Leveraging WordPress’s built-in user roles to represent client tiers (as shown in the activation hook).
In the myplugin_get_product_pricing function, we retrieve the user’s client_id and check for a corresponding _contracted_price_{client_id} meta key on the product. This provides the highest level of pricing specificity.
Frontend Integration and Security Considerations
On the frontend, JavaScript will be responsible for fetching pricing data from the /myplugin/v1/pricing/product/{id} endpoint. It’s critical to ensure that the API requests are made with appropriate authentication (e.g., using nonce verification for logged-in users).
// Example of how to enqueue a script that uses the REST API
add_action( 'wp_enqueue_scripts', function() {
wp_enqueue_script( 'myplugin-pricing', get_template_directory_uri() . '/js/pricing.js', array( 'wp-api' ), '1.0', true );
// Pass data to the script, including the REST API URL and nonce
wp_localize_script( 'myplugin-pricing', 'myplugin_ajax_object', array(
'ajax_url' => rest_url( 'myplugin/v1/pricing/product/' ),
'nonce' => wp_create_nonce( 'wp_rest' ),
) );
} );
The JavaScript code would then look something like this:
jQuery(document).ready(function($) {
var productId = 123; // Example product ID
$.ajax({
url: myplugin_ajax_object.ajax_url + productId,
method: 'GET',
beforeSend: function ( xhr ) {
// Add nonce for authenticated requests
xhr.setRequestHeader( 'X-WP-Nonce', myplugin_ajax_object.nonce );
}
})
.done(function(response) {
console.log('Pricing data:', response);
// Logic to display pricing based on response.effective_price, response.volume_discounts, etc.
if (response.effective_price) {
$('#product-price').text('Your Price: $' + response.effective_price);
}
// Further logic for volume discounts, etc.
})
.fail(function(jqXHR, textStatus, errorThrown) {
console.error('Error fetching pricing:', textStatus, errorThrown);
// Handle errors, e.g., display a message to the user
});
});
Security Best Practices:
- Nonce Verification: Always use nonces for any requests that modify data or require authentication. The
wp_create_nonce( 'wp_rest' )andX-WP-Nonceheader are standard for REST API interactions. - Capability Checks: Rigorously check capabilities for any write operations. For read operations, implement checks within the callback if the data is sensitive.
- Input Sanitization: Sanitize all data received via API requests (as demonstrated with
sanitize_text_fieldand custom sanitizers) and all data before saving it to the database. - HTTPS: Ensure your WordPress site uses HTTPS to protect data in transit.
- Role Management: Regularly audit user roles and capabilities to ensure least privilege.
Conclusion
By combining custom REST API endpoints for granular metadata management with robust role and capability-based access control, you can build a secure and highly adaptable B2B pricing grid system within WordPress. This approach provides the flexibility needed for complex pricing structures while maintaining the integrity and security of your business data.