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.