• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Building secure B2B pricing grids with custom WordPress Database Class ($wpdb) endpoints and role overrides

Building secure B2B pricing grids with custom WordPress Database Class ($wpdb) endpoints and role overrides

Leveraging $wpdb for Secure B2B Pricing Grids

Building dynamic, role-specific pricing grids within WordPress for B2B clients demands a robust and secure approach. Relying solely on front-end logic or basic user meta is insufficient for sensitive data like pricing. This guide details how to create custom WordPress REST API endpoints that interact directly with the database via the global $wpdb object, implementing granular role-based access control to ensure data integrity and security.

Database Schema Design for Pricing Grids

A well-structured database is paramount. For a B2B pricing grid, we’ll consider a simplified schema. We’ll need a table to store product information, another for pricing tiers (which can be linked to user roles or custom meta), and potentially a mapping table if pricing is complex (e.g., per-customer pricing). For this example, we’ll assume a `products` table and a `pricing_tiers` table, where `pricing_tiers` can be associated with specific WordPress roles.

Let’s define the SQL for our hypothetical tables. This would typically be managed via a plugin’s activation hook.

Creating Product and Pricing Tables

-- Table for products
CREATE TABLE wp_b2b_products (
    id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
    name VARCHAR(255) NOT NULL,
    sku VARCHAR(100) UNIQUE NOT NULL,
    description TEXT,
    PRIMARY KEY  (id),
    KEY sku (sku)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Table for pricing tiers
CREATE TABLE wp_b2b_pricing_tiers (
    id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
    role VARCHAR(64) NOT NULL, -- Corresponds to WordPress user roles (e.g., 'wholesale_customer', 'reseller')
    product_id BIGINT(20) UNSIGNED NOT NULL,
    price DECIMAL(10, 2) NOT NULL,
    min_quantity INT(11) UNSIGNED DEFAULT 1,
    PRIMARY KEY  (id),
    FOREIGN KEY (product_id) REFERENCES wp_b2b_products(id) ON DELETE CASCADE,
    UNIQUE KEY role_product (role, product_id) -- Ensure only one price per role/product
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Registering Custom REST API Endpoints

We’ll use the `register_rest_route` function to expose our pricing data. This function is the cornerstone of extending the WordPress REST API. We’ll create an endpoint to fetch product pricing based on the current user’s role.

Endpoint for Fetching Pricing Data

This PHP code should be placed within your plugin’s main file or an included file. It registers a route under `/myplugin/v1/pricing/products`.

add_action( 'rest_api_init', function () {
    register_rest_route( 'myplugin/v1', '/pricing/products', array(
        'methods'  => 'GET',
        'callback' => 'myplugin_get_b2b_pricing',
        'permission_callback' => function () {
            // This is crucial for security. Only authenticated users can access.
            // Further role checks will be done in the callback.
            return is_user_logged_in();
        }
    ) );
});

function myplugin_get_b2b_pricing( WP_REST_Request $request ) {
    global $wpdb;
    $table_products = $wpdb->prefix . 'b2b_products';
    $table_pricing = $wpdb->prefix . 'b2b_pricing_tiers';

    // Get current user's roles
    $user_id = get_current_user_id();
    $user_roles = wp_get_current_user()->roles;

    // Determine the most specific role for pricing lookup.
    // This logic can be complex. For simplicity, we'll take the first role found that has pricing defined.
    // A more robust system might prioritize certain roles (e.g., 'administrator' > 'reseller' > 'wholesale_customer').
    $pricing_role = null;
    if ( ! empty( $user_roles ) ) {
        foreach ( $user_roles as $role ) {
            // Check if this role has any pricing defined in our table
            $role_exists = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table_pricing} WHERE role = %s LIMIT 1", $role ) );
            if ( $role_exists ) {
                $pricing_role = $role;
                break; // Found a role with pricing, use it.
            }
        }
    }

    // If no specific pricing role is found, maybe fall back to a default or deny access.
    // For this example, we'll return an empty array if no pricing role is identified.
    if ( ! $pricing_role ) {
        return new WP_Error( 'rest_forbidden', esc_html__( 'No applicable pricing tier found for your account.', 'myplugin' ), array( 'status' => 403 ) );
    }

    // Fetch all products and their prices for the determined role
    $sql = $wpdb->prepare(
        "SELECT
            p.id,
            p.name,
            p.sku,
            p.description,
            pt.price,
            pt.min_quantity
        FROM {$table_products} AS p
        JOIN {$table_pricing} AS pt ON p.id = pt.product_id
        WHERE pt.role = %s",
        $pricing_role
    );

    $results = $wpdb->get_results( $sql );

    if ( $wpdb->last_error ) {
        return new WP_Error( 'db_error', esc_html__( 'Database query failed.', 'myplugin' ), array( 'status' => 500 ) );
    }

    if ( empty( $results ) ) {
        return new WP_Error( 'no_data', esc_html__( 'No pricing data available for your role.', 'myplugin' ), array( 'status' => 404 ) );
    }

    // Format the results for the API response
    $formatted_results = array_map( function( $item ) {
        return array(
            'id'         => (int) $item->id,
            'name'       => $item->name,
            'sku'        => $item->sku,
            'description' => $item->description,
            'price'      => (float) $item->price,
            'min_quantity' => (int) $item->min_quantity,
        );
    }, $results );

    return rest_ensure_response( $formatted_results );
}

Implementing Role-Based Access Control (RBAC)

The security of this endpoint hinges on two key areas: the `permission_callback` and the logic within the main callback function. The `permission_callback` ensures only authenticated users can even attempt to access the endpoint. The main callback then performs the crucial role-checking.

Advanced Role Prioritization and Fallbacks

In a real-world B2B scenario, a user might have multiple roles (e.g., ‘administrator’ and ‘reseller’). The logic for determining which pricing tier to apply needs to be sophisticated. A common strategy is to define a priority order for roles. For instance, if a user is both an ‘administrator’ and a ‘reseller’, you might want to apply the ‘administrator’ pricing if it’s more favorable or strictly defined.

Here’s an enhanced version of the role determination logic:

function myplugin_get_b2b_pricing( WP_REST_Request $request ) {
    global $wpdb;
    $table_products = $wpdb->prefix . 'b2b_products';
    $table_pricing = $wpdb->prefix . 'b2b_pricing_tiers';

    $user_id = get_current_user_id();
    $user_roles = wp_get_current_user()->roles;

    // Define a priority order for roles. Higher index means higher priority.
    // Ensure these roles exist in your WordPress setup.
    $role_priority = array(
        'administrator'      => 10,
        'super_admin'        => 11, // WordPress core role
        'wholesale_customer' => 5,
        'reseller'           => 7,
        // Add other custom roles as needed
    );

    $best_match_role = null;
    $highest_priority = -1;

    if ( ! empty( $user_roles ) ) {
        foreach ( $user_roles as $role ) {
            if ( isset( $role_priority[$role] ) && $role_priority[$role] > $highest_priority ) {
                // Check if this role actually has pricing defined
                $pricing_exists = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table_pricing} WHERE role = %s LIMIT 1", $role ) );
                if ( $pricing_exists ) {
                    $highest_priority = $role_priority[$role];
                    $best_match_role = $role;
                }
            }
        }
    }

    // If no role with defined pricing is found, return an error.
    if ( ! $best_match_role ) {
        return new WP_Error( 'rest_forbidden', esc_html__( 'No applicable pricing tier found for your account.', 'myplugin' ), array( 'status' => 403 ) );
    }

    // Now use $best_match_role for the query
    $sql = $wpdb->prepare(
        "SELECT
            p.id,
            p.name,
            p.sku,
            p.description,
            pt.price,
            pt.min_quantity
        FROM {$table_products} AS p
        JOIN {$table_pricing} AS pt ON p.id = pt.product_id
        WHERE pt.role = %s",
        $best_match_role // Use the determined highest priority role
    );

    // ... rest of the function (query execution, error handling, formatting) ...
    $results = $wpdb->get_results( $sql );

    if ( $wpdb->last_error ) {
        return new WP_Error( 'db_error', esc_html__( 'Database query failed.', 'myplugin' ), array( 'status' => 500 ) );
    }

    if ( empty( $results ) ) {
        // This case should ideally not be hit if $best_match_role was found, but good for safety.
        return new WP_Error( 'no_data', esc_html__( 'No pricing data available for your role.', 'myplugin' ), array( 'status' => 404 ) );
    }

    $formatted_results = array_map( function( $item ) {
        return array(
            'id'         => (int) $item->id,
            'name'       => $item->name,
            'sku'        => $item->sku,
            'description' => $item->description,
            'price'      => (float) $item->price,
            'min_quantity' => (int) $item->min_quantity,
        );
    }, $results );

    return rest_ensure_response( $formatted_results );
}

Securing $wpdb Queries

Directly using $wpdb requires meticulous attention to security, primarily SQL injection prevention. The `prepare()` method is your best friend here. It sanitizes and escapes data before it’s inserted into SQL queries, preventing malicious input from altering query logic.

Best Practices for $wpdb Usage

  • Always use $wpdb->prepare() for any dynamic data in your queries, especially user-supplied or role-based data.
  • Use appropriate placeholders: %s for strings, %d for integers, %f for floats.
  • Be mindful of table prefixes. Always use $wpdb->prefix to ensure your plugin works correctly across different WordPress installations.
  • Sanitize any data that might be written back to the database (though this example focuses on reading). Use functions like sanitize_text_field(), sanitize_email(), etc.
  • Validate input from the REST API request parameters, even if not used directly in this read-only example.

Frontend Integration and Usage

On the frontend, you can use JavaScript (e.g., with jQuery or vanilla JS) to fetch data from this endpoint. The fetch request should include appropriate authentication headers if your WordPress site uses nonces for REST API authentication (which it should for logged-in users).

Example JavaScript Fetch Request

jQuery(document).ready(function($) {
    // Ensure you have the nonce available, typically passed from PHP
    // Example: wp_localize_script( 'my-script', 'myPluginData', array( 'rest_url' => rest_url(), 'nonce' => wp_create_nonce( 'wp_rest' ) ) );
    var restUrl = myPluginData.rest_url + 'myplugin/v1/pricing/products';
    var nonce = myPluginData.nonce;

    $.ajax({
        url: restUrl,
        method: 'GET',
        beforeSend: function (xhr) {
            xhr.setRequestHeader('X-WP-Nonce', nonce);
        },
        success: function(data) {
            if (data && data.length > 0) {
                console.log('Pricing Data:', data);
                // Render the pricing grid here
                var pricingHtml = '<table><thead><tr><th>Product</th><th>SKU</th><th>Price</th><th>Min Qty</th></tr></thead><tbody>';
                data.forEach(function(item) {
                    pricingHtml += '<tr>';
                    pricingHtml += '<td>' + item.name + '</td>';
                    pricingHtml += '<td>' + item.sku + '</td>';
                    pricingHtml += '<td>' + item.price.toFixed(2) + '</td>'; // Format price
                    pricingHtml += '<td>' + item.min_quantity + '</td>';
                    pricingHtml += '</tr>';
                });
                pricingHtml += '</tbody></table>';
                $('#pricing-grid-container').html(pricingHtml);
            } else {
                $('#pricing-grid-container').html('<p>No pricing information available.</p>');
            }
        },
        error: function(jqXHR, textStatus, errorThrown) {
            console.error('Error fetching pricing data:', textStatus, errorThrown, jqXHR.responseJSON);
            var errorMessage = 'Could not load pricing information.';
            if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
                errorMessage = jqXHR.responseJSON.message;
            }
            $('#pricing-grid-container').html('<p style="color: red;">' + errorMessage + '</p>');
        }
    });
});

Conclusion

By combining the power of the WordPress REST API with direct, secure $wpdb interactions and robust role-based access control, you can build sophisticated and secure B2B pricing solutions. Always prioritize security by using $wpdb->prepare(), validating user roles, and ensuring proper authentication for your API endpoints. This approach provides a scalable and maintainable foundation for managing complex pricing structures within WordPress.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Reducing database query bloat in Sage Roots modern environments layouts using custom lazy loaders
  • Performance Optimization: Tuning PHP-FPM and opcache pools for high-concurrency Firebase Realtime DB handlers
  • Reducing Largest Contentful Paint (LCP) by optimizing custom script enqueuing structures in legacy plugins
  • How to implement native Redis caching layers for high-volume custom taxonomy queries in Carbon Fields custom wrappers
  • Building secure B2B pricing grids with custom REST API Controllers endpoints and role overrides

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (658)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (872)
  • PHP (5)
  • PHP Development (48)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (182)
  • WordPress Plugin Development (197)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • Reducing database query bloat in Sage Roots modern environments layouts using custom lazy loaders
  • Performance Optimization: Tuning PHP-FPM and opcache pools for high-concurrency Firebase Realtime DB handlers
  • Reducing Largest Contentful Paint (LCP) by optimizing custom script enqueuing structures in legacy plugins

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (872)
  • Debugging & Troubleshooting (658)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala