Skip to content
CHADA Store

Building a custom rule

Custom rules are full citizens — they get a slider on the Rule weights tab, contribute to the score, appear in the score detail modal, and store their metadata for chargeback rebuttals.

Minimal rule class

namespace MyTheme;

use WooFraudGuardRiskEngineContractsRuleInterface;
use WooFraudGuardRiskEngineContractsRuleResult;
use WooFraudGuardRiskEngineOrderContext;
use WooFraudGuardRiskEngineReasonsReasonRenderer;

final class HolidayShippingRule implements RuleInterface {
    public function key(): string { return \'holiday_shipping\'; }
    public function label(): string { return __( \'Holiday-window shipping\', \'my-theme\' ); }
    public function defaultWeight(): float { return 0.5; }

    public function evaluate( OrderContext $ctx ): RuleResult {
        // Catch addresses billed to one country but shipped to a known
        // package-forwarder city during peak holiday weeks.
        $forwarder_cities = [ \'sterling\', \'wilmington\', \'doral\' ];
        if ( ! in_array( strtolower( (string) $ctx->shippingCity ), $forwarder_cities, true ) ) {
            return RuleResult::clean();
        }
        $week = (int) date( \'W\' );
        if ( $week < 46 || $week > 52 ) {
            return RuleResult::clean();
        }
        return RuleResult::risky(
            55.0,
            ReasonRenderer::encode( [
                ReasonRenderer::token( \'address.shipping_to_forwarder_city\' ),
            ] ),
            [ \'city\' => $ctx->shippingCity, \'week\' => $week ]
        );
    }
}

Register the rule

add_action( \'woofraudguard_register_rules\', function ( $registry ) {
    $registry->register( new MyThemeHolidayShippingRule() );
} );

After the next page load, your rule appears in Settings → Rule weights with a slider defaulting to 0.5. Its reason token (address.shipping_to_forwarder_city in this example) needs a corresponding entry in your theme\’s translation catalog if you want localized labels.

Best practices

  • Return RuleResult::clean() liberally. The base score is the rule\’s maximum potential signal — most orders should score zero on most rules.
  • Store useful diagnostics in $metadata. They show up in the score detail modal\’s Details expander and are invaluable for tuning.
  • Use ReasonRenderer::token() + ReasonRenderer::encode() for reason text. That keeps your rule\’s persisted reasons language-neutral and they\’ll render in the merchant\’s locale automatically.
  • Cap base scores at 100. Anything above is clamped on persist anyway, but emitting 200 makes the rule logic harder to reason about.