How to Build a Quote and Tracking WordPress Plugin with Paystack Payments

Are you looking for a simple way to allow users to request quotes, track their quotes, and make payments via Paystack? In this tutorial, we will create a Tracking Quote Plugin for WordPress that enables users to:

✔️ Request a service quote
✔️ Track their quote using a unique ID
✔️ Make payments securely using Paystack
✔️ Manage quotes easily from the WordPress admin panel

Let’s build this plugin from scratch in just three files:

  • tracking-quote.php (Main Plugin)

  • style.css (Stylesheet)

  • script.js (JavaScript for AJAX and Paystack integration)


Step 1: Create the Plugin Folder

Navigate to your WordPress wp-content/plugins/ directory and create a new folder called tracking-quote.

Inside this folder, create the following files:

📄 tracking-quote.php
📄 style.css
📄 script.js


Step 2: Main Plugin File (tracking-quote.php)

This file initializes the plugin, registers shortcodes, handles form submissions, and integrates Paystack payments.

🔹 Full Code for tracking-quote.php

<?php
/**
 * Plugin Name: Tracking Quote Plugin
 * Description: Allows users to request quotes and track them using a reference ID.
 * Version: 1.0
 * Author: Ogunuyo Ogheneruemu
 */

if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}

// **Activate Plugin: Create database table**
function tq_create_table() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'tracking_quotes';
    $charset_collate = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE IF NOT EXISTS $table_name (
        id INT NOT NULL AUTO_INCREMENT,
        tracking_id VARCHAR(10) NOT NULL UNIQUE,
        fullname VARCHAR(255) NOT NULL,
        email VARCHAR(255) NOT NULL,
        pickup_location VARCHAR(255) NOT NULL,
        destination VARCHAR(255) NOT NULL,
        additional_message TEXT,
        status ENUM('Quote Received', 'Awaiting Payment', 'Cancelled', 'Completed', 'Pending') DEFAULT 'Pending',
        amount DECIMAL(10,2) DEFAULT 0.00,
        current_location VARCHAR(255) DEFAULT '',
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id)
    ) $charset_collate;";

    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
}
register_activation_hook(__FILE__, 'tq_create_table');

function tq_enqueue_scripts() {
    wp_enqueue_style('bootstrap-css', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css');
    wp_enqueue_script('bootstrap-js', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js', array('jquery'), false, true);
    wp_enqueue_style('tq-style', plugin_dir_url(__FILE__) . 'style.css');
    wp_enqueue_script('tq-script', plugin_dir_url(__FILE__) . 'script.js', array('jquery'), false, true);
    wp_enqueue_script('paystack-js', 'https://js.paystack.co/v1/inline.js', array(), false, true); // Paystack script

    wp_localize_script('tq-script', 'tq_ajax', array('ajax_url' => admin_url('admin-ajax.php')));

}
add_action('wp_enqueue_scripts', 'tq_enqueue_scripts');

function tq_generate_tracking_id($length = 8) {
    return strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, $length));
}

function tq_submit_quote() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'tracking_quotes';

    $fullname = sanitize_text_field($_POST['fullname']);
    $email = sanitize_email($_POST['email']);
    $pickup_location = sanitize_text_field($_POST['pickup_location']);
    $destination = sanitize_text_field($_POST['destination']);
    $additional_message = sanitize_textarea_field($_POST['additional_message']);

    $tracking_id = tq_generate_tracking_id();

    $wpdb->insert($table_name, [
        'tracking_id' => $tracking_id,
        'fullname' => $fullname,
        'email' => $email,
        'pickup_location' => $pickup_location,
        'destination' => $destination,
        'additional_message' => $additional_message
    ]);




    echo json_encode(['status' => 'success', 'tracking_id' => $tracking_id]);
    wp_die();
}
add_action('wp_ajax_tq_submit_quote', 'tq_submit_quote');
add_action('wp_ajax_nopriv_tq_submit_quote', 'tq_submit_quote');


function tq_track_quote() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'tracking_quotes';

    $tracking_id = sanitize_text_field($_POST['tracking_id']);
    $quote = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE tracking_id = %s", $tracking_id));

    if ($quote) {
        wp_send_json_success([
            'fullname' => $quote->fullname,
            'email' => $quote->email,
            'created_at' => $quote->created_at,
            'pickup_location' => $quote->pickup_location,
            'current_location' => !empty($quote->current_location) ? $quote->current_location : 'Not Updated',
            'destination' => $quote->destination,
            'status' => $quote->status,
            'amount' => number_format($quote->amount, 2), // Ensure proper formatting
            'tracking_id' => $quote->tracking_id // Ensure this is included!
        ]);
    } else {
        wp_send_json_error(['message' => 'Tracking ID not found.']);
    }
}
add_action('wp_ajax_tq_track_quote', 'tq_track_quote');
add_action('wp_ajax_nopriv_tq_track_quote', 'tq_track_quote');



function tq_display_quote_form() {
    ob_start(); ?>
    <div class="tq-form-container">
        <h3>Request a Quote</h3>
        <form id="tq-quote-form">
            <input type="text" id="tq-fullname" placeholder="Full Name" required>
            <input type="email" id="tq-email" placeholder="Email" required>
            <input type="text" id="tq-pickup-location" placeholder="Pickup Location" required>
            <input type="text" id="tq-destination" placeholder="Destination" required>
            <textarea id="tq-additional-message" placeholder="Additional Message"></textarea>
            <button type="submit">Submit Quote</button>
        </form>
        <div id="tq-tracking-id"></div>

        <h3>Track Your Quote</h3>
        <input type="text" id="tq-track-id" placeholder="Enter Tracking ID">
        <button id="tq-track-btn">Track</button>
        <div id="tq-track-result"></div>
    </div>
    <?php return ob_get_clean();
}
add_shortcode('tracking_quote', 'tq_display_quote_form');


function tq_display_request_quote_form() {
    ob_start(); ?>
    <div class="container mt-5">
        <div class="card p-4 shadow-sm">
            <h3 class="text-center">Request a Quote</h3>
            <form id="tq-quote-form" class="needs-validation" novalidate>
                <div class="mb-3">
                    <input type="text" id="tq-fullname" class="form-control" placeholder="Full Name" required>
                </div>
                <div class="mb-3">
                    <input type="email" id="tq-email" class="form-control" placeholder="Email" required>
                </div>
                <div class="mb-3">
                    <input type="text" id="tq-pickup-location" class="form-control" placeholder="Pickup Location" required>
                </div>
                <div class="mb-3">
                    <input type="text" id="tq-destination" class="form-control" placeholder="Destination" required>
                </div>
                <div class="mb-3">
                    <textarea id="tq-additional-message" class="form-control" placeholder="Additional Message"></textarea>
                </div>
                <button type="submit" class="btn btn-primary w-100">Submit Quote</button>
                <button type="reset" class="btn btn-secondary w-100 mt-2" onclick="document.getElementById('tq-quote-form').reset();">Reset</button>
            </form>
            <div id="tq-tracking-id"></div>
        </div>
    </div>
    <?php return ob_get_clean();
}
add_shortcode('request_quote_form', 'tq_display_request_quote_form');

function tq_display_track_quote_form() {
    ob_start(); ?>
    <div class="container mt-5">
        <div class="card p-4 shadow-sm">
            <h3 class="text-center">Track Your Quotes</h3>
            <div class="input-group mb-3">
                <input type="text" id="tq-track-id" class="form-control" placeholder="Enter Tracking ID">
                <button id="tq-track-btn" class="btn btn-secondary">Track</button>
                <button type="reset" class="btn btn-secondary ms-2" onclick="document.getElementById('tq-track-id').value = '';">Reset</button>
            </div>
            <div id="tq-track-result"></div>
        </div>
    </div>
    <?php return ob_get_clean();
}
add_shortcode('track_quote_form', 'tq_display_track_quote_form');


function tq_confirm_payment() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'tracking_quotes';

    $tracking_id = sanitize_text_field($_POST['tracking_id']);
    $transaction_ref = sanitize_text_field($_POST['transaction_ref']);

    // Verify Payment via Paystack API
    $paystack_secret_key = "sk_live_xxxxxxxx"; // Replace with your Paystack secret key
    $url = "https://api.paystack.co/transaction/verify/{$transaction_ref}";

    $args = array(
        'headers' => array(
            'Authorization' => 'Bearer ' . $paystack_secret_key,
            'Content-Type'  => 'application/json'
        )
    );

    $response = wp_remote_get($url, $args);
    $body = wp_remote_retrieve_body($response);
    $result = json_decode($body);

    if ($result->status && $result->data->status === "success") {
        // Payment is successful, update the status
        $wpdb->update(
            $table_name,
            array('status' => 'Paid'),
            array('tracking_id' => $tracking_id)
        );

        wp_send_json_success(["message" => "Payment verified and status updated to Paid"]);
    } else {
        wp_send_json_error(["message" => "Payment verification failed"]);
    }
}
add_action('wp_ajax_tq_confirm_payment', 'tq_confirm_payment');
add_action('wp_ajax_nopriv_tq_confirm_payment', 'tq_confirm_payment');




function tq_admin_menu() {
    add_menu_page('Manage Quotes', 'Quote Manager', 'manage_options', 'tq-quotes', 'tq_admin_page', 'dashicons-admin-comments', 20);
}
add_action('admin_menu', 'tq_admin_menu');


//start here


function tq_admin_page() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'tracking_quotes';

    // Handle Add or Update Tracking
    if (isset($_POST['submit_quote'])) {
        $fullname = sanitize_text_field($_POST['fullname']);
        $email = sanitize_email($_POST['email']);
        $pickup_location = sanitize_text_field($_POST['pickup_location']);
        $destination = sanitize_text_field($_POST['destination']);
        $additional_message = sanitize_textarea_field($_POST['additional_message']);
        $status = sanitize_text_field($_POST['status']);
        $amount = sanitize_text_field($_POST['amount']);

        $tracking_id = tq_generate_tracking_id();


        // $name = sanitize_text_field($_POST['name']);
        // $email = sanitize_email($_POST['email']);
        // $comment = sanitize_textarea_field($_POST['comment']);
        $comment_id = isset($_POST['comment_id']) ? intval($_POST['comment_id']) : 0;

        if ($comment_id > 0) {
            // Update existing comment
            $wpdb->update($table_name, ['fullname' => $fullname, 'email' => $email, 'pickup_location' => $pickup_location, 'destination' => $destination, 'additional_message' => $additional_message, 'status' => $status , 'amount' => $amount], ['id' => $comment_id]);
            echo "<div class='updated'><p>Comment updated successfully!</p></div>";
        } else {
            // Insert new comment
            $wpdb->insert($table_name, ['fullname' => $fullname, 'email' => $email, 'pickup_location' => $pickup_location, 'destination' => $destination, 'additional_message' => $additional_message, 'tracking_id' => $tracking_id, 'status' => $status , 'amount' => $amount  ]);
            echo "<div class='updated'><p>Comment added successfully!</p></div>";
        }
    }

    // Handle Delete Tracking
    if (isset($_GET['delete'])) {
        $delete_id = intval($_GET['delete']);
        $wpdb->delete($table_name, ['id' => $delete_id]);
        echo "<div class='updated'><p>Quote deleted successfully!</p></div>";
    }

    // Fetch comments
    $quotes = $wpdb->get_results("SELECT * FROM $table_name ORDER BY created_at DESC");

    ?>
<div>


    <div class="wrap">
        <h2>Manage Quotes</h2>
        <form method="POST">
            <input type="hidden" name="comment_id" id="comment_id" value="">
            <table class="form-table">
                <tr>
                    <th><label for="fullname">Fullame</label></th>
                    <td><input type="text" name="fullname" id="fullname" class="regular-text" required></td>
                </tr>
                <tr>
                    <th><label for="email">Email</label></th>
                    <td><input type="email" name="email" id="email" class="regular-text" required></td>
                </tr>
                <tr>
                    <th><label for="pickup_location">Pickup Location</label></th>
                    <td><input type="text" name="pickup_location" id="pickup_location" class="regular-text" required></td>
                </tr>
                <tr>
                    <th><label for="destination">Destination</label></th>
                    <td><input type="text" name="destination" id="destination" class="regular-text" required></td>
                </tr>
                <tr>
                    <th><label for="additional_message">Additional Message</label></th>
                    <td><textarea name="additional_message" id="additional_message" class="regular-text"></textarea></td>
                </tr>
                <tr>
                    <th><label for="status">Status</label></th>
                    <td>
                        <select name="status" id="status" class="regular-text">
                            <option value="Pending">Pending</option>
                            <option value="Quote Received">Quote Received</option>
                            <option value="Awaiting Payment">Awaiting Payment</option>
                            <option value="Paid">Paid</option>
                            <option value="Cancelled">Cancelled</option>
                            <option value="Completed">Completed</option>
                        </select>
                    </td>
                </tr> 
                <tr>
                    <th><label for="amount">Amount</label></th>
                    <td><input type="text" name="amount" id="amount" class="regular-text" required></td>
                </tr> 
            </table>
            <p>
                <button type="submit" name="submit_quote" class="button button-primary">Save Quote</button>
            </p>
        </form>

        <h3>Quote List</h3>
        <table class="wp-list-table widefat fixed striped">
            <thead>
                <tr>
                    <th>Tracking ID</th>
                    <th>Fullname</th>
                    <th>Email</th>
                    <th>Pick-Up Location</th>
                    <th>Destination</th>
                    <th>Additional Message</th>
                    <th>Status</th>
                    <th>Amount</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($quotes as $quote) : ?>
                    <tr>
                    <td><?php echo esc_html($quote->tracking_id); ?></td>
                        <td><?php echo esc_html($quote->fullname); ?></td>
                        <td><?php echo esc_html($quote->email); ?></td>
                        <td><?php echo esc_html($quote->pickup_location); ?></td>
                        <td><?php echo esc_html($quote->destination); ?></td>
                        <td><?php echo esc_html($quote->additional_message); ?></td>
                        <td><?php echo esc_html($quote->status); ?></td>
                        <td><?php echo esc_html($quote->amount); ?></td>
                        <td>
                            <a href="javascript:void(0);" onclick="editQuote(<?php echo $quote->id; ?>, '<?php echo esc_js($quote->fullname); ?>', '<?php echo esc_js($quote->email); ?>', '<?php echo esc_js($quote->pickup_location); ?>' , '<?php echo esc_js($quote->destination); ?>' , '<?php echo esc_js($quote->additional_message); ?>' , '<?php echo esc_js($quote->status); ?>' , '<?php echo esc_js($quote->amount); ?>');" class="button">Edit</a>
                            <a href="?page=tq-quotes&delete=<?php echo $quote->id; ?>" class="button button-danger" onclick="return confirm('Are you sure?');">Delete</a>
                        </td>
                    </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
    </div>



    <script>
        function editQuote(id, fullname, email, pickup_location, destination, additional_message, status, amount) {
            document.getElementById('comment_id').value = id;
            document.getElementById('fullname').value = fullname;
            document.getElementById('email').value = email;
            document.getElementById('pickup_location').value = pickup_location;
            document.getElementById('destination').value = destination;
            document.getElementById('additional_message').value = additional_message;
            document.getElementById('status').value = status; 
            document.getElementById('amount').value = amount; 
        }
    </script>
    <?php
}

Step 3: Styling (style.css)

.tq-form-container {
    width: 100%;
    max-width: 600px;
    margin: auto;
    padding: 20px;
    background: #f9f9f9;
    border-radius: 8px;
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}

.tq-form-container input,
.tq-form-container textarea {
    width: 100%;
    padding: 10px;
    margin-bottom: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
}

.tq-form-container button {
    width: 100%;
    background: #007bff;
    color: white;
    padding: 12px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-size: 16px;
}

.tq-form-container button:hover {
    background: #0056b3;
}

.tq-result-box {
    background: #fff;
    padding: 15px;
    border-radius: 5px;
    margin-top: 15px;
    border-left: 4px solid #007bff;
}

Step 4: JavaScript for AJAX and Paystack (script.js)

jQuery(document).ready(function($) {
    console.log("Script Loaded"); // Debugging

    // Submit Quote Form
    $('#tq-quote-form').on('submit', function(e) {
        e.preventDefault();

        var fullname = $('#tq-fullname').val().trim();
        var email = $('#tq-email').val().trim();
        var pickup_location = $('#tq-pickup-location').val().trim();
        var destination = $('#tq-destination').val().trim();
        var additional_message = $('#tq-additional-message').val().trim();

        if (!fullname || !email || !pickup_location || !destination) {
            alert("All fields are required!");
            return;
        }

        $.ajax({
            type: 'POST',
            url: tq_ajax.ajax_url,
            data: {
                action: 'tq_submit_quote',
                fullname: fullname,
                email: email,
                pickup_location: pickup_location,
                destination: destination,
                additional_message: additional_message
            },
            success: function(response) {
                var data = JSON.parse(response);
                if (data.status === 'success') {
                    $('#tq-tracking-id').html(`<p>Tracking ID: <strong>${data.tracking_id}</strong></p>`);
                } else {
                    alert('Error submitting quote.');
                }
            },
            error: function() {
                alert('AJAX request failed.');
            }
        });
    });

     // Track Quote
     $('#tq-track-btn').on('click', function() {
        var tracking_ids = $('#tq-track-id').val().trim();

        if (tracking_ids === '') {
            $('#tq-track-result').html('<p style="color:red;">Please enter a tracking ID.</p>');
            return;
        }

        $.ajax({
            type: 'POST',
            url: tq_ajax.ajax_url,
            data: {
                action: 'tq_track_quote',
                tracking_id: tracking_ids
            },
            dataType: 'json',
            success: function(response) {
                if (response.success) {
                    console.log("Tracking ID Found:", response.data.tracking_id);
                    var paymentButton = "";

                    if (response.data.status === "Awaiting Payment") {
                        paymentButton = `<button class="btn btn-primary mt-3 pay-now-button" 
                                            data-tracking-id="${response.data.tracking_id}" 
                                            data-amount="${response.data.amount}" 
                                            data-email="${response.data.email}">Pay Now</button>`;
                    }

                    $('#tq-track-result').html(`
                        <div class="tq-result-box">
                            <p><strong>Full Name:</strong> ${response.data.fullname}</p>
                            <p><strong>Email:</strong> ${response.data.email}</p>
                            <p><strong>Created At:</strong> ${response.data.created_at}</p>
                            <p><strong>Pickup Location:</strong> ${response.data.pickup_location}</p>
                            <p><strong>Current Location:</strong> ${response.data.current_location}</p>
                            <p><strong>Destination:</strong> ${response.data.destination}</p>
                            <p><strong>Amounts:</strong> N${response.data.amount}</p>
                            <p><strong>Tracking ID: </strong> ${response.data.tracking_id}</p>
                            <p><strong>Status:</strong> ${response.data.status}</p>

                            ${paymentButton}
                        </div>
                    `);
                } else {
                    $('#tq-track-result').html(`<p style="color:red;">${response.data.message}</p>`);
                }
            },
            error: function() {
                $('#tq-track-result').html('<p style="color:red;">An error occurred. Please try again.</p>');
            }
        });
    });

    // Handle Pay Now Button Click
    $(document).on("click", ".pay-now-button", function() {
        var trackingId = $(this).data("tracking-id");
        var amount = $(this).data("amount");
        var email = $(this).data("email");

        console.log(`Pay Now Clicked! Tracking ID: ${trackingId}, Amount: ${amount}, Email: ${email}`);

        processPayment(trackingId, amount, email);
    });

    // Function to Process Payment (Paystack)
    window.processPayment = function(trackingId, amount, email) {
        console.log(`Processing payment for Tracking ID: ${trackingId}, Amount: N${amount}, Email: ${email}`);

        var handler = PaystackPop.setup({
            key: 'pk_live_xxxxxxxxx', // Replace with your Paystack public key
            email: email,
            amount: amount * 100, // Convert to kobo
            currency: 'NGN', // Change to your preferred currency
            callback: function(response) {
                console.log("Payment Successful ✅:", response);
                updatePaymentStatus(trackingId, response.reference);
            },
            onClose: function() {
                alert("Transaction cancelled.");
            }
        });

        handler.openIframe();
    };

    // Function to Update Payment Status After Successful Transaction
    function updatePaymentStatus(trackingId, transactionRef) {
        $.ajax({
            type: 'POST',
            url: tq_ajax.ajax_url,
            data: {
                action: 'tq_confirm_payment',
                tracking_id: trackingId,
                transaction_ref: transactionRef
            },
            dataType: 'json',
            success: function(response) {
                if (response.success) {
                    alert("Payment Successful! ✅");
                    $('#tq-track-btn').click(); // Refresh tracking details
                } else {
                    alert("Payment verification failed.");
                }
            },
            error: function() {
                alert("Error updating payment status.");
            }
        });
    }






});

Conclusion

🚀 Your WordPress Quote Tracking Plugin is now ready! 🚀

With this simple 3-file setup, you now have a fully functional quote tracking system that allows users to request quotes, track them, and process payments using Paystack.

🔹 Next Steps:

  • Test your plugin by installing it in WordPress

  • Improve the admin panel for managing quotes

  • Add more automation, such as email notifications

0
Subscribe to my newsletter

Read articles from Ogunuyo Ogheneruemu B directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ogunuyo Ogheneruemu B
Ogunuyo Ogheneruemu B

I'm Ogunuyo Ogheneruemu Brown, a senior software developer. I specialize in DApp apps, fintech solutions, nursing web apps, fitness platforms, and e-commerce systems. Throughout my career, I've delivered successful projects, showcasing strong technical skills and problem-solving abilities. I create secure and user-friendly fintech innovations. Outside work, I enjoy coding, swimming, and playing football. I'm an avid reader and fitness enthusiast. Music inspires me. I'm committed to continuous growth and creating impactful software solutions. Let's connect and collaborate to make a lasting impact in software development.