13 KiB
Coupon Validator
Validates WooCommerce coupons and provides enhanced context to PayPal's Agentic Commerce API.
Quick Reference
Core validation features:
- Case-insensitive coupon code matching via
wc_sanitize_coupon_code() - Comprehensive WooCommerce coupon validation (expiry, usage limits, restrictions)
- Enhanced context for failed validations (eligible items, shortage amounts, discount comparisons)
- Uses
WC_Discountsfor accurate discount calculation - Minimal resolution options by default (suggested alternatives disabled unless explicitly enabled via filter)
How It Works
What it validates:
- Coupon existence and expiration
- Usage limits (global and per-user)
- Minimum/maximum order amounts
- Product/category restrictions
- Email restrictions
- Coupon stacking rules
- WooCommerce coupon system status
Validation flow:
- Filter coupons with
APPLYaction only (skipREMOVEactions) - Normalize coupon codes using
wc_sanitize_coupon_code()for case-insensitive matching - Check if coupons are enabled in WooCommerce via
wc_coupons_enabled() - Check for coupon stacking conflicts (individual_use coupons)
- Validate each coupon via WooCommerce's
WC_Discounts::is_coupon_valid() - Capture numeric error codes via
woocommerce_coupon_errorfilter and map to PayPal issue types - Build enhanced context via declarative
context_buildersconfiguration - Apply filters to enrich data
WooCommerce Integration:
- Uses
wc_sanitize_coupon_code()to normalize coupon codes (case-insensitive matching) - Uses
WC_Discountsfor validation and discount calculation - Uses
WC_Coupon::is_valid_for_product()for product eligibility checking - Uses
wc_price()for currency formatting, respecting merchant's currency position settings - Uses
wc_format_decimal()for consistent decimal formatting - Automatically supports third-party plugins that extend WooCommerce coupon functionality
- Stacking conflicts: By default, WooCommerce allows multiple coupons unless "Individual use only" is enabled
- When
individual_use=true, the coupon cannot be combined with ANY other coupons
Issue Types
| Issue Type | Triggered When |
|---|---|
COUPON_EXPIRED |
Expiration date passed |
COUPON_NOT_EXIST |
Invalid/non-existent code |
MINIMUM_ORDER_NOT_MET |
Cart total below minimum |
MAXIMUM_ORDER_EXCEEDED |
Cart total above maximum |
COUPON_NOT_APPLICABLE |
Product/category restrictions not met |
USAGE_LIMIT_EXCEEDED |
Global or per-user limit hit |
COUPON_STACKING_NOT_ALLOWED |
Can't combine coupons (individual_use) |
COUPON_ALREADY_APPLIED |
Duplicate application |
COUPON_EMAIL_RESTRICTED |
Email restriction failed |
COUPON_INVALID |
Generic invalid coupon |
COUPON_NOT_SUPPORTED |
Coupons disabled in WooCommerce |
Error Mapping:
The validator maps WooCommerce errors to PayPal issue types using numeric error constants (100-116) from WC_Coupon. We use the woocommerce_coupon_error filter to capture the numeric error code when WC_Discounts::is_coupon_valid() fails.
Since WooCommerce error messages are localized (esc_html__), we cannot rely on pattern matching - instead we capture the numeric error code via the filter, making the validation work correctly for all languages.
Enhanced Context Fields
Context automatically includes:
// Basic context (always included)
'specific_issue' => 'COUPON_EXPIRED',
'coupon_code' => 'SUMMER50',
// For minimum order not met
'minimum_required' => '100.00',
'current_subtotal' => '19.99',
'shortage_amount' => '80.01',
// For product restrictions
'eligible_items' => array( 'PRODUCT-A', 'PRODUCT-B' ),
// For stacking conflicts
'current_discount' => '32.00',
'attempted_discount' => '9.99',
Note: Suggested alternative coupons are disabled by default. Enable them via filter:
add_filter(
'woocommerce_paypal_payments_agentic_commerce_suggested_alternative_coupons',
function ( $alternatives, $failed_code, $specific_issue, $cart ) {
// Return array of alternative coupon codes
return array( 'SUMMER20', 'WELCOME15', 'FALL10' );
},
10,
4
);
API Response Examples
Successful Coupon Application
{
"id": "CART-123",
"status": "READY",
"validation_status": "VALID",
"validation_issues": [],
"applied_coupons": [
{
"code": "GAMING20",
"description": "20% off gaming accessories",
"discount_amount": {"currency_code": "USD", "value": "32.00"}
}
],
"totals": {
"item_total": {"currency_code": "USD", "value": "159.98"},
"discount": {"currency_code": "USD", "value": "32.00"},
"shipping": {"currency_code": "USD", "value": "9.99"},
"tax_total": {"currency_code": "USD", "value": "11.52"},
"amount": {"currency_code": "USD", "value": "127.98"}
}
}
Failed Coupon Validation (Default Response)
{
"id": "CART-123",
"status": "INCOMPLETE",
"validation_status": "INVALID",
"validation_issues": [
{
"code": "PRICING_ERROR",
"type": "BUSINESS_RULE",
"message": "Coupon has expired",
"user_message": "The coupon code 'SUMMER50' has expired.",
"field": "coupons[0]",
"context": {
"specific_issue": "COUPON_EXPIRED",
"coupon_code": "SUMMER50",
"expiration_date": "2024-05-31T23:59:59Z"
},
"resolution_options": [
{
"action": "REMOVE_COUPON",
"label": "Remove coupon and continue",
"metadata": {"priority": "low"}
}
]
}
]
}
Note: By default, resolution options are minimal (usually just REMOVE_COUPON). Actions like SUGGEST_ALTERNATIVE_COUPON, VIEW_AVAILABLE_COUPONS, and TRY_DIFFERENT_COUPON are not included by default because:
- Most WooCommerce stores don't expose available coupons publicly
- Suggesting alternatives requires custom logic to determine which coupons are actually available
- These options should be explicitly enabled via the
woocommerce_paypal_payments_agentic_commerce_coupon_validation_resolutionsfilter
Filter Integration
Three filters allow third-party plugins (Advanced Coupons, Smart Coupons, etc.) to extend validation.
These filters complement WooCommerce's native filters:
- WooCommerce filters (
woocommerce_coupon_is_valid,woocommerce_coupon_error, etc.) control core validation logic - Our filters enrich the API response structure and provide AI agent guidance
- Third-party plugins can use both: WC filters for validation, our filters for Agentic Commerce features
1. Modify Resolution Options
Add or change resolution actions:
add_filter(
'woocommerce_paypal_payments_agentic_commerce_coupon_validation_resolutions',
function ( $resolutions, $issue_type, $code, $wc_coupon, $cart, $context ) {
if ( $issue_type === 'COUPON_NOT_APPLICABLE' ) {
$resolutions[] = array(
'action' => 'EARN_POINTS',
'label' => 'Complete a purchase to earn points',
'metadata' => array( 'priority' => 'high' ),
);
}
return $resolutions;
},
10,
6
);
2. Customize User Message
Personalize error messages (richer context than WooCommerce's woocommerce_coupon_error):
add_filter(
'woocommerce_paypal_payments_agentic_commerce_coupon_validation_user_message',
function ( $user_message, $issue_type, $code, $wc_coupon, $cart, $context ) {
$name = $cart->customer()['name'] ?? 'there';
return "Hi {$name}! " . $user_message;
},
10,
6
);
3. Control Alternative Coupons
Filter suggested alternatives:
add_filter(
'woocommerce_paypal_payments_agentic_commerce_suggested_alternative_coupons',
function ( $alternatives, $failed_code, $specific_issue, $cart ) {
if ( is_vip_customer( $cart ) ) {
array_unshift( $alternatives, 'VIP25' );
}
return array_slice( $alternatives, 0, 5 );
},
10,
4
);
Complete Plugin Example
<?php
/**
* Plugin Name: Advanced Coupons - PayPal Agentic Integration
*/
class ACFW_PayPal_Agentic_Integration {
public function __construct() {
add_filter(
'woocommerce_coupon_is_valid',
array( $this, 'validate_bogo' ),
10,
3
);
add_filter(
'woocommerce_paypal_payments_agentic_commerce_coupon_validation_resolutions',
array( $this, 'add_bogo_resolutions' ),
10,
6
);
}
public function validate_bogo( $valid, $wc_coupon, $discounts ) {
$bogo = get_post_meta( $wc_coupon->get_id(), '_acfw_bogo_deals', true );
if ( empty( $bogo ) ) {
return $valid;
}
$cart_items = WC()->cart->get_cart();
$has_trigger = false;
foreach ( $cart_items as $item ) {
if ( $item['product_id'] === $bogo['trigger_product'] ) {
$has_trigger = true;
break;
}
}
if ( ! $has_trigger ) {
throw new Exception(
sprintf( 'Add %s to unlock this BOGO deal.', get_the_title( $bogo['trigger_product'] ) ),
WC_Coupon::E_WC_COUPON_NOT_APPLICABLE
);
}
return $valid;
}
public function add_bogo_resolutions( $resolutions, $issue_type, $code, $wc_coupon, $cart, $context ) {
if ( $issue_type !== 'COUPON_NOT_APPLICABLE' || ! $wc_coupon ) {
return $resolutions;
}
$bogo = get_post_meta( $wc_coupon->get_id(), '_acfw_bogo_deals', true );
if ( $bogo ) {
array_unshift( $resolutions, array(
'action' => 'ADD_PRODUCT',
'label' => sprintf( 'Add %s to unlock BOGO deal', get_the_title( $bogo['trigger_product'] ) ),
'metadata' => array(
'priority' => 'high',
'product_id' => $bogo['trigger_product'],
),
) );
}
return $resolutions;
}
}
new ACFW_PayPal_Agentic_Integration();
Best Practices
Use Both WooCommerce and Agentic Filters
Our filters complement WooCommerce's native filters:
add_filter( 'woocommerce_coupon_is_valid', function( $valid, $coupon, $discounts ) {
if ( ! custom_check( $coupon ) ) {
return false;
}
return $valid;
}, 10, 3 );
add_filter( 'woocommerce_paypal_payments_agentic_commerce_coupon_validation_resolutions',
function( $resolutions, $issue_type, $code, $wc_coupon, $cart, $context ) {
$resolutions[] = array(
'action' => 'CUSTOM_ACTION',
'label' => 'Custom resolution for this issue',
);
return $resolutions;
}, 10, 6
);
Preserve Existing Data
When filtering, always preserve existing array keys:
$context['custom_field'] = 'value';
return $context;
Check Null Values
WooCommerce coupon object can be null:
if ( ! $wc_coupon ) {
return $result;
}
Use Specific Issue Types
Filter only relevant validation scenarios:
if ( $specific_issue !== 'COUPON_EXPIRED' ) {
return $context;
}
Clear Resolution Labels
Provide actionable guidance for AI agents:
'label' => 'Add $50.00 more to qualify'
Implementation Details
Architecture:
Split into focused, single-responsibility classes:
| Class | Responsibility |
|---|---|
| CouponValidator | Orchestrates validation flow, checks stacking conflicts, delegates to specialists |
| CouponContextBuilder | Builds enriched context data using declarative pattern, uses WC native methods |
| DiscountCalculator | Handles discount calculation via WC_Discounts, uses wc_format_decimal() |
| CouponResolutionBuilder | Builds resolution options from templates, handles stacking-specific resolutions |
| AppliedCouponsBuilder | Builds applied_coupons response data for successfully validated coupons |
Declarative Configuration:
Each issue type declares what it needs via ISSUE_CONFIG:
'MINIMUM_ORDER_NOT_MET' => array(
'message' => 'Minimum order amount not met',
'user_message' => "The coupon '%s' requires a minimum order of %s. Your current order is %s.",
'resolutions' => array( 'add_items_to_minimum', 'continue_without' ),
'context_builders' => array( 'minimum_spend', 'eligible_items' ),
),
Context builders are small focused methods in CouponContextBuilder:
build_expiration()- Adds expiration datebuild_usage_limits()- Adds usage limit/countbuild_minimum_spend()- Adds minimum required, current subtotal, shortage amountbuild_maximum_spend()- Adds maximum allowedbuild_eligible_items()- Adds list of eligible product variant IDsbuild_stacking()- Adds stacking conflict details with discount comparisonbuild_email_restriction()- Adds email restriction flag
Note: Alternative coupon suggestions are handled separately via the woocommerce_paypal_payments_agentic_commerce_suggested_alternative_coupons filter, not as a declarative context builder.
Response Integration:
ResponseFactoryusesAppliedCouponsBuilderto build coupon data before creating response objects- Response DTOs (
CartResponse,NewCartResponse,PaidCartResponse) receiveapplied_couponsas plain array data - This keeps response classes as pure DTOs without business logic
AppliedCouponsBuilder::calculate_total_discount()provides discount totals for PayPal order updates