Recipe: add a German registry (end to end)

  1. 1. The driver
  2. 2. Register it + add a check
  3. 3. Score DE differently
  4. 4. Wire the verifier
  5. 5. Certify

Goal: verify DE counterparties against a Handelsregister-style API, then score the result with a DE-only rule.

1. The driver

use Gawrys\Counterparty\Registry\{AbstractRegistryDriver, LookupRequest, LookupResult};
use Gawrys\Counterparty\Enum\RegistryCapability;
use Gawrys\Counterparty\Http\JsonHttpClient;
use Gawrys\Counterparty\Support\ArrayReader;

final readonly class GermanRegistryDriver extends AbstractRegistryDriver
{
    public function __construct(private JsonHttpClient $http, private string $apiKey) {}

    public function capabilities(): array { return [RegistryCapability::LegalEntityData]; }
    public function countries(): array    { return ['DE']; }

    public function lookup(LookupRequest $request): LookupResult
    {
        $taxId = $request->counterparty->euVat ?? $request->counterparty->nip;
        if ($taxId === null) {
            return LookupResult::notFound('https://handelsregister.de/');
        }

        $json = $this->http->getJson(
            "https://handelsregister.example/v1/company?vat={$taxId}",
            ['Authorization' => 'Bearer ' . $this->apiKey],
        );
        $r = ArrayReader::of($json);

        if (!$r->has('company')) {
            return LookupResult::notFound('https://handelsregister.de/');
        }

        $c = $r->nested('company');

        return LookupResult::found(
            data: ['legalName' => $c->string('name'), 'status' => $c->string('status')],
            proofId: $c->string('hrb'),
            sourceUrl: 'https://handelsregister.de/',
        );
    }
}

2. Register it + add a check

use Gawrys\Counterparty\Registry\RegistryManager;
use Gawrys\Counterparty\Check\RegistryCheck;

$registries = new RegistryManager();
$registries->extend('de', fn () => new GermanRegistryDriver($http, $deKey));

$checks = [
    // ... White List / VIES / sanctions ...
    new RegistryCheck($registries, RegistryCapability::LegalEntityData, $clock, 'de.handelsregister', 'DE Handelsregister'),
];

3. Score DE differently

use Gawrys\Counterparty\Risk\{RiskRule, RiskContext, RiskSignal, Evidence};

final class GermanDissolvedRule implements RiskRule
{
    public function evaluate(RiskContext $ctx): iterable
    {
        if ($ctx->counterparty->country !== 'DE') {
            return;
        }
        foreach ($ctx->report->fromSource('de.handelsregister') as $r) {
            if (($r->raw['status'] ?? null) === 'DISSOLVED') {
                yield new RiskSignal('de.dissolved', 0.8, adverse: true,
                    evidence: Evidence::grounded('Company dissolved per Handelsregister.', $r->proofId ?? 'https://handelsregister.de/', 0.9));
            }
        }
    }
}

4. Wire the verifier

$verifier = new Verifier(
    $checks,
    new RuleBasedRiskStrategy([
        new GermanDissolvedRule(),
        ...iterator_to_array((function () { yield from [
            new \Gawrys\Counterparty\Risk\Rule\SanctionsHitRule(),
            new \Gawrys\Counterparty\Risk\Rule\InconclusiveCoverageRule(),
        ]; })()),
    ]),
    $clock,
);

5. Certify

Add a GermanRegistryDriverTest extends RegistryDriverContractTestCase (see Contract tests) wired to a mocked HTTP client. Done - DE is a first-class country with its own scoring, and nothing in the core changed.


Counterparty Verification - a due-diligence aid, not a compliance product. MIT licensed.

This site uses Just the Docs, a documentation theme for Jekyll.