Skip to content

Adding a provider check

The path each existing check follows. Roughly in order:

1. Client

app/lib/providers/<provider>/client.rb — builds requests, applies credentials from configuration, parses responses.

ruby
class Error < Providers::Error; end
class ConfigurationError < Error; end
class RequestFailed < Error; end

Inherit from Providers::Error

ProviderFetchRun.capture! rescues that class. An error outside the hierarchy escapes as a 500 and no failed run is recorded — the failure leaves no trace at all. An invariant test walks the provider directories and asserts the ancestry, so getting this wrong fails the suite rather than production.

2. Adapter

app/lib/providers/<provider>/<check>_adapter.rb, subclassing BaseAdapter:

  • #fetch — call the client, or return an injected payload in tests
  • #normalize — provider shape to Verity's
  • .public_result — the sanitised result shown to operators and returned by the API

Rules for normalize and public_result:

  • Missing is nil, not a default
  • An empty collection means the provider said empty, not that we did not ask
  • A positive claim (verified, valid, clear) needs a positive signal in the payload, never merely the absence of an error

3. Register it

EntityCheck::OPERATOR_CHECK_TYPES and PUBLIC_RESULT_ADAPTERS. Then, as applicable:

ConstantAdd when
MANUAL_ENTITY_CHECK_TYPESAn operator can run it from the entity page
ASYNC_ENTITY_CHECK_TYPESIt completes asynchronously
ENTITY_CREATE_DEFAULT_CHECK_TYPESIt may be configured as an intake default

4. Resolve configuration

A branch in ProviderFetchRuns::ConfigResolver: provider key, fetch type, lookup identifier, adapter, projection strategy.

Enforce preconditions here, with a message an operator can act on, rather than sending a request you know will fail.

5. Project, or do not

If the check should write into Verity's records, add a projector. If its value is the evidence itself, use NoOpProjector — most of the newer checks do.

6. Surface it

  • A label in config/locales/en.yml under entity_checks.manual_labels
  • If it produces structured evidence, make the fetch-run page render it: add its keys to STRUCTURED_NORMALIZED_SUMMARY_KEYS, or a reader if the summary is flat

This step is easy to miss

IBAN verification stores its fields flat rather than nested, matched no structured key, and the evidence page reported no normalised evidence while printing those same fields as raw JSON below it.

7. Test it

  • Adapter tests with recorded payloads: a documented success, an error, and a sparse or null response
  • A controller test for the operator path
  • Confirm the invariant suite still passes — it will hold your public_result to the empty-payload rule automatically

8. Document it

Add a row to the check catalogue, and to docs/provider-operability-runbook.md with the required configuration and common failure modes.