Custom rules
- A signal
- Example: a high-risk-jurisdiction rule
- Composing with the defaults
- Reading check data in a rule
- Rules must be pure
A RiskRule inspects the hard facts and emits zero or more RiskSignals. You add rules to
RuleBasedRiskStrategy without subclassing it.
interface RiskRule
{
/** @return iterable<RiskSignal> */
public function evaluate(RiskContext $context): iterable;
}
RiskContext gives you both the counterparty and the full report.
A signal
new RiskSignal(
code: 'geo.high_risk', // stable machine id, shown in the summary
weight: 0.6, // 0.0-1.0; clamped if out of range
adverse: false, // true = hard negative finding -> forces review
evidence: Evidence::ungrounded('Registered in a high-risk jurisdiction.', 0.7),
);
Example: a high-risk-jurisdiction rule
use Gawrys\Counterparty\Risk\{RiskRule, RiskContext, RiskSignal, Evidence};
final class HighRiskCountryRule implements RiskRule
{
/** @param list<string> $countries */
public function __construct(private array $countries) {}
public function evaluate(RiskContext $context): iterable
{
if (in_array($context->counterparty->country, $this->countries, true)) {
// adverse:false on purpose - a high-risk jurisdiction is a contributing risk
// FACTOR (raises the score via weight), not a hard-negative finding. Set
// adverse:true only for facts that must force human review on their own
// (e.g. a sanctions hit).
yield new RiskSignal('geo.high_risk', 0.6, adverse: false,
evidence: Evidence::ungrounded('High-risk jurisdiction.', 0.7));
}
}
}
Composing with the defaults
withDefaultRules() is just a convenience. To mix your rule with the bundled ones, list them
explicitly:
use Gawrys\Counterparty\Risk\RuleBasedRiskStrategy;
use Gawrys\Counterparty\Risk\Rule\{SanctionsHitRule, VatStatusRule, BankAccountMismatchRule, InconclusiveCoverageRule};
$strategy = new RuleBasedRiskStrategy([
new SanctionsHitRule(),
new VatStatusRule(),
new BankAccountMismatchRule(),
new InconclusiveCoverageRule(),
new HighRiskCountryRule(['XX', 'YY']),
], reviewThreshold: 0.5);
Reading check data in a rule
Rules find results by source and read the raw payload an adapter produced:
foreach ($context->report->fromSource('pl.white_list') as $r) {
if (($r->raw['bankAccountAssigned'] ?? null) === false) {
yield new RiskSignal('bank_account.unassigned', 0.7, adverse: true);
}
}
Rules must be pure
No I/O, no clock, no randomness - given the same context a rule must return the same
signals. This keeps an assessment reproducible and cache-friendly. Do network work in a
Check (which becomes a fact in the report); rules only interpret facts.