WooCommerce Plugin Development | Chapter 8 | Discount Plugin

by | Dec 9, 2025 | Plugin Development, Web App Development | 0 comments

Chapter 8: Limit Number of Uses Per Customer 🔒

This chapter is critical for controlling coupon distribution by allowing you to set a maximum usage limit per customer.

Our goal is to implement the following logic for logged-in users:

  • Check Usage: Determine how many times the customer has previously used the coupon.

  • Enforce Limit: If the customer’s usage equals or exceeds the Max uses per customer limit, the discount is blocked with a clear error message.

  • Increment Count: If the discount is applied, the usage count is incremented once the order reaches the Completed status.

We’ll proceed in four steps:

  • Extend the coupon data structure and admin UI for the new field.

  • Add helper functions to manage user-specific coupon usage in user meta.

  • Hook into order creation/completion to track and store usage.

  • Update the discount logic to enforce the limit.

1. Extend Coupon Data: max_uses_per_customer

We start by modifying our existing functions to include the new max_uses_per_customer field.

1.1. Update the “Add New Coupon” Save Handler

In your sdr_handle_settings_form_submit() function, update the ADD NEW COUPON logic to capture and validate the new sdr_max_uses_per_customer field.

Replace the existing block (around where sdr_customer_type is handled) with the following to add the new field handling:

/**
 * Handle settings form submission (message + enable/disable).
 */
function sdr_handle_settings_form_submit() {

    // Check if our form was submitted.
    if ( ! isset( $_POST['sdr_settings_submit'] ) ) {
        return;
    }

    // Check nonce for security.
    if ( ! isset( $_POST['sdr_settings_nonce'] ) || ! wp_verify_nonce( $_POST['sdr_settings_nonce'], 'sdr_save_settings' ) ) {
        return;
    }

    // Check capability.
    if ( ! current_user_can( 'manage_woocommerce' ) ) {
        return;
    }

    // Checkout message (optional helper text).
    $message = isset( $_POST['sdr_checkout_message'] )
        ? wp_kses_post( wp_unslash( $_POST['sdr_checkout_message'] ) )
        : '';

    // Enable discount (checkbox) – acts as "Enable All Discounts".
    $enable_discount = isset( $_POST['sdr_enable_discount'] ) && 'yes' === $_POST['sdr_enable_discount']
        ? 'yes'
        : 'no';

    // Save options.
    update_option( 'sdr_checkout_message', $message );
    update_option( 'sdr_enable_discount', $enable_discount );

    // Add a settings updated flag so we can show a notice.
    add_settings_error(
        'sdr_messages',
        'sdr_message',
        __( 'Settings saved.', 'simple-discount-rules' ),
        'updated'
    );
}
add_action( 'admin_init', 'sdr_handle_settings_form_submit' );

1.2. Add a field to the “Add New Coupon” form

In `sdr_render_settings_page()`, inside the **ADD NEW COUPON FORM** table, add this row before the “Status” row:


/**
 * Render the plugin settings page (helper message + global toggle).
 */
function sdr_render_settings_page() {

    // Get current values from the options table.
    $current_message = get_option(
        'sdr_checkout_message',
        __( 'Enter your special discount code at checkout to claim the offer.', 'simple-discount-rules' )
    );

    $enable_discount = get_option( 'sdr_enable_discount', 'no' );

    // Show any saved messages (from add_settings_error).
    settings_errors( 'sdr_messages' );
    ?>

    <div class="wrap">
        <h1><?php esc_html_e( 'Simple Discount Rules – Settings', 'simple-discount-rules' ); ?></h1>

        <form method="post" action="">
            <?php wp_nonce_field( 'sdr_save_settings', 'sdr_settings_nonce' ); ?>

            <h2><?php esc_html_e( 'General Message', 'simple-discount-rules' ); ?></h2>

            <table class="form-table" role="presentation">
                <tbody>
                    <tr>
                        <th scope="row">
                            <label for="sdr_checkout_message">
                                <?php esc_html_e( 'Checkout Helper Message', 'simple-discount-rules' ); ?>
                            </label>
                        </th>
                        <td>
                            <textarea
                                name="sdr_checkout_message"
                                id="sdr_checkout_message"
                                rows="4"
                                cols="50"
                                class="large-text"
                            ><?php echo esc_textarea( $current_message ); ?></textarea>
                            <p class="description">
                                <?php esc_html_e( 'Shown near the discount code field on the checkout page. You can use it to explain the offer or conditions.', 'simple-discount-rules' ); ?>
                            </p>
                        </td>
                    </tr>
                </tbody>
            </table>

            <h2><?php esc_html_e( 'Global Discount Toggle', 'simple-discount-rules' ); ?></h2>

            <table class="form-table" role="presentation">
                <tbody>
                    <tr>
                        <th scope="row">
                            <?php esc_html_e( 'Enable All Discounts', 'simple-discount-rules' ); ?>
                        </th>
                        <td>
                            <label>
                                <input
                                    type="checkbox"
                                    name="sdr_enable_discount"
                                    value="yes"
                                    <?php checked( $enable_discount, 'yes' ); ?>
                                />
                                <?php esc_html_e( 'Enable all Simple Discount Rules coupons.', 'simple-discount-rules' ); ?>
                            </label>

                            <p class="description">
                                <?php esc_html_e( 'Uncheck to temporarily disable all custom discount coupons, even if they are marked as active.', 'simple-discount-rules' ); ?>
                            </p>
                        </td>
                    </tr>
                </tbody>
            </table>

            <?php submit_button( __( 'Save Changes', 'simple-discount-rules' ), 'primary', 'sdr_settings_submit' ); ?>

        </form>
    </div>

    <?php
}

1.3. Show the  the coupon list

In the **Existing Coupons** table header, add a new column:


/* ----------------------------------------------------------
   11. APPLY MULTIPLE COUPON RULES (REPLACES OLD SINGLE-COUPON LOGIC)
---------------------------------------------------------- */
/**
 * Apply a discount when a matching active coupon rule is entered.
 * Uses:
 *  - sdr_enable_discount (global ON/OFF)
 *  - sdr_coupons (array of rule arrays)
 *  - Session key sdr_coupon_code (captured from checkout field)
 */
function sdr_apply_multi_coupon( $cart ) {

    if ( ! sdr_is_woocommerce_active() ) {
        return;
    }

    // Don't run in admin (except AJAX used by WC).
    if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
        return;
    }

    // Global toggle.
    $enabled = get_option( 'sdr_enable_discount', 'no' );
    if ( 'yes' !== $enabled ) {
        return;
    }

    // Get entered code from session.
    $entered_code = '';
    if ( class_exists( 'WC' ) && WC()->session ) {
        $entered_code = WC()->session->get( 'sdr_coupon_code', '' );
    }

    if ( empty( $entered_code ) ) {
        return; // no code entered
    }

    // Load all coupon rules.
    $coupons = sdr_get_all_coupons();
    if ( empty( $coupons ) ) {
        if ( isset( $_POST['sdr_coupon_code'] ) ) {
            wc_add_notice( __( 'The discount code you entered is not valid.', 'simple-discount-rules' ), 'error' );
        }
        return;
    }

    // Find matching ACTIVE coupon (case-insensitive).
    $rule = false;
    foreach ( $coupons as $c ) {
        if (
            isset( $c['code'], $c['status'] ) &&
            0 === strcasecmp( $entered_code, $c['code'] ) &&
            'active' === $c['status']
        ) {
            $rule = $c;
            break;
        }
    }

    $just_submitted = isset( $_POST['sdr_coupon_code'] ); // For showing notices only once.

    if ( ! $rule ) {
        if ( $just_submitted ) {
            wc_add_notice(
                __( 'The discount code you entered is not valid or is inactive.', 'simple-discount-rules' ),
                'error'
            );
        }
        return;
    }

    // Normalize rule values.
    $discount_type = isset( $rule['discount_type'] ) ? $rule['discount_type'] : 'flat';
    $discount_val  = isset( $rule['value'] ) ? floatval( $rule['value'] ) : 0;
    $min_order     = isset( $rule['min'] ) ? floatval( $rule['min'] ) : 0;
    $max_order     = isset( $rule['max'] ) ? floatval( $rule['max'] ) : 0;
    $customer_type = isset( $rule['customer_type'] ) ? $rule['customer_type'] : 'any';
    $max_uses_per_customer = isset( $rule['max_uses_per_customer'] ) ? intval( $rule['max_uses_per_customer'] ) : 0;
    $coupon_code_label = isset( $rule['code'] ) ? $rule['code'] : '';

    if ( $discount_val <= 0 ) { return; } $cart_subtotal = floatval( $cart->get_subtotal() );

    // Min order check.
    if ( $min_order > 0 && $cart_subtotal < $min_order ) { if ( $just_submitted ) { $msg = sprintf( __( 'This discount is only valid for orders of at least %s.', 'simple-discount-rules' ), wc_price( $min_order ) ); wc_add_notice( $msg, 'error' ); } return; } // Max order check. if ( $max_order > 0 && $cart_subtotal > $max_order ) {
        if ( $just_submitted ) {
            $msg = sprintf(
                __( 'This discount is only valid for orders up to %s.', 'simple-discount-rules' ),
                wc_price( $max_order )
            );
            wc_add_notice( $msg, 'error' );
        }
        return;
    }

    // Customer eligibility (any / new / existing).
    list( $eligible, $error_message ) = sdr_is_customer_eligible( $customer_type );
    if ( ! $eligible ) {
        if ( $just_submitted && ! empty( $error_message ) ) {
            wc_add_notice( $error_message, 'error' );
        }
        return;
    }

    // Enforce per-customer usage limit (if set).
    if ( $max_uses_per_customer > 0 ) {

        $user_id = get_current_user_id();

        // Require login so we can enforce usage per user.
        if ( ! $user_id ) {
            if ( $just_submitted ) {
                wc_add_notice( __( 'Please log in to use this discount.', 'simple-discount-rules' ), 'error' );
            }
            return;
        }

        $already_used = sdr_get_user_coupon_usage( $user_id, $coupon_code_label );

        if ( $already_used >= $max_uses_per_customer ) {
            if ( $just_submitted ) {
                wc_add_notice( __( 'You have already used this discount the maximum number of times.', 'simple-discount-rules' ), 'error' );
            }
            return;
        }
    }

    // Calculate discount.
    $discount = 0.0;

    if ( 'flat' === $discount_type ) {
        $discount = min( $discount_val, $cart_subtotal );
    } elseif ( 'percentage' === $discount_type ) {
        if ( $discount_val > 0 ) {
            $discount = ( $cart_subtotal * $discount_val ) / 100;
            if ( $discount > $cart_subtotal ) {
                $discount = $cart_subtotal;
            }
        }
    } else {
        // Unknown type, do nothing.
        return;
    }

    if ( $discount <= 0 ) { return; } // Build label that reflects the type. if ( 'percentage' === $discount_type ) { $label = sprintf( /* translators: 1: coupon code, 2: discount percentage */ __( 'Special Discount (%1$s – %2$s%%)', 'simple-discount-rules' ), esc_html( $coupon_code_label ), esc_html( $discount_val ) ); } else { $label = sprintf( /* translators: %s: coupon code */ __( 'Special Discount (%s)', 'simple-discount-rules' ), esc_html( $coupon_code_label ) ); } // Add a negative fee (discount) to the cart. $cart->add_fee( $label, - $discount ); // Negative value = discount.

    // Show success notice only when code was just submitted.
    if ( $just_submitted ) {
        if ( 'percentage' === $discount_type ) {
            $notice = sprintf(
                __( 'Your %s%% discount has been applied!', 'simple-discount-rules' ),
                esc_html( $discount_val )
            );
        } else {
            $notice = __( 'Your special discount has been applied!', 'simple-discount-rules' );
        }
        wc_add_notice( $notice, 'success' );
    }
}
add_action( 'woocommerce_cart_calculate_fees', 'sdr_apply_multi_coupon', 20, 1 );

Test Scenarios:

🧪 Scenario A – One-Time Welcome Coupon

Create Coupon Settings

  • Code: WELCOME10

  • Discount Type: Percentage

  • Value: 10%

  • Minimum Order Amount: ₹1000

  • Customer Type: New users only

  • Max Uses Per Customer: 1

  • Status: Active

How to Test

Log in as a new test user and place an order using WELCOME10.

  • Discount should apply.
  • After order completion, the user’s usage count becomes 1.

Place a second order using the same code.

  • The plugin should block it with the message:
    “You have already used this discount the maximum number of times.”
  • No discount should be applied.

 

🧪 Scenario B – Three-Time VIP Coupon

Create Coupon Settings

  • Code: VIP50

  • Type: Percentage

  • Value: 50%

  • Customer Type: Existing customers (optional)

  • Max Uses Per Customer: 3

  • Status: Active

How to Test

For any logged-in user:

  • Apply the coupon in three separate completed orders → Discount should work each time.

  • Attempt to use it a fourth time → The system should block it and display:
    “You have already used this discount the maximum number of times.”

🧪 Scenario C – Unlimited Usage Coupon

Create Coupon Settings

  • Code: SAVE5

  • Max Uses Per Customer: 0 or leave blank

  • This means unlimited usage allowed (as long as other rules match).

Use it any number of times → The discount should always apply.

✨ Quick Recap of What You Built

In this chapter, you improved your WooCommerce discount rules plugin by adding per-user usage limits. You implemented:

✔ Extended coupon data:

  • Added max_uses_per_customer to each coupon.

✔ Admin Panel Enhancements:

  • New field in “Add Coupon” form

  • Displayed in the coupon list

✔ User-Based Tracking:

  • Stored each user’s coupon usage in user_meta.

✔ Order Integration:

  • Saved the applied coupon inside each WooCommerce order.

  • Incremented usage when the order reached Completed status.

✔ Checkout Validation:

  • Blocked discounts exceeding the allowed limit.

  • Displayed helpful error messages to the customer.

🎉 Final Result

Your plugin now supports a realistic and commonly used rule:

“WELCOME10 provides 10% off, valid only for new customers, one time per customer, and only on orders above ₹1000.”

Perfect for real-world eCommerce setups and fully customizable for any coupon strategy.

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *