open-standards/openpay/docs/24-driver-sdk-progress.md
Transaction Science 70ecd6caa0 Initial import — three open standards under one shelf
Transaction Science open standards monorepo. Each subdirectory is a
self-contained reference protocol with its own README and licence.

- openpay/    Apache-2.0  payment-acceptance stack (Rust, 26 crates)
- smart-byte/ CC-BY-4.0   content-addressed value carrier (spec)
- eoc/        CC-BY-4.0   energy-optimised compute (spec)

The wire format and the right to fork are public. The optional hosted
services are the offer; the protocols are not.
2026-05-23 07:58:09 -04:00

181 lines
8.2 KiB
Markdown

# Phase 24 — Driver SDK: operators can author their own rails
**Status**: Draft v0.24
**Date**: 2026-05-22
## Why
OpenPay's whole architecture is built around pluggable rail
adapters (`RailAdapter`, `CardAcquirer`, `A2aAcquirer`). What it
*didn't* have until Phase 24 was the **author-side scaffolding** —
the deterministic mocks operators use to bring up a deployment
without live credentials, and the conformance harness that lets
driver authors verify their adapter respects the trait's
behavioral contract beyond what the type system enforces.
Final phase of the sequenced trio (22 → 23 → 24) that closes out
the operator-facing surface: settlement (Phase 22), HTTP API
(Phase 23), driver SDK (Phase 24). After this an operator with
real PSP credentials can write a driver, run conformance, register
it with the orchestrator, and deploy.
## What shipped
| # | Item | Where |
|--:|---|---|
| 1 | `op-driver-sdk` crate — lib + author guide module docs | `crates/op-driver-sdk/src/lib.rs` |
| 2 | `DeterministicCardAcquirer` — programmable card acquirer: per-key overrides, amount-threshold rules, transport-error mode, request history | `card.rs` |
| 3 | `DeterministicA2aGateway` — symmetric programmable A2A gateway with UETR overrides | `a2a.rs` |
| 4 | `ConformanceFailure` taxonomy + `ConformanceReport` aggregate | `conformance.rs` |
| 5 | `conformance::run_card` — battery of contract checks for any `CardAcquirer` | `conformance.rs` |
| 6 | `conformance::run_a2a` — equivalent for `A2aAcquirer` | `conformance.rs` |
| 7 | `conformance::run_card_with_panic_probe` — stronger variant via `catch_unwind` | `conformance.rs` |
| 8 | Orchestrator integration tests — deterministic drivers flowing through a real `Orchestrator` via `CardAdapter` / `A2aAdapter` | `tests/orchestrator_integration.rs` |
| 9 | Unit tests proving the harness catches malformed drivers (lying `supports()`, duplicate PSP ids, empty name, missing UETR) | `conformance.rs::tests` |
Workspace at the end of Phase 24:
| Check | Result |
|---|---|
| `cargo build --workspace --all-targets` | **0 errors, 0 warnings** |
| `cargo test --workspace` | **930 passing, 0 failing** (+24 vs Phase 23) |
| `cargo clippy --workspace --all-targets` | **0 warnings** |
Test-count delta: 19 unit (card 8, a2a 5, conformance 6) + 5 e2e
integration = 24.
## The driver-author flow
```text
1. impl CardAcquirer for MyPspClient { ... }
2. Write at least one happy-path test against your real PSP
sandbox.
3. Run op_driver_sdk::conformance::run_card(&my_client)?
to catch structural bugs.
4. Wrap your acquirer in op_orchestrator::CardAdapter and register
with the orchestrator.
```
In code:
```rust
let driver = MyPspClient::sandbox(api_key);
let report = op_driver_sdk::conformance::run_card(&driver);
assert!(report.is_clean(), "driver failed: {:?}", report.failures);
let card = Arc::new(CardAdapter::new("my-psp", Arc::new(driver)));
orchestrator.register_adapter(card);
```
## What the conformance harness covers
| Check | Failure | What it catches |
|---|---|---|
| `name()` non-empty | `EmptyName` | Telemetry / routing keys off `name()`; empty breaks operator dashboards |
| `supports()` ↔ `authorize()` consistency | `SupportsLies` | A misclassifying driver causes the orchestrator to route to a rail that then 4xx's every request |
| `psp_payment_id` non-empty on success | `EmptyPspPaymentId` | Capture / refund / void all require it |
| `psp_payment_id` unique across calls | `DuplicatePspPaymentId` | Reused ids corrupt later capture / refund routing |
| `authorized_amount` or `error_code` present on success | `AuthorizeResponseEmpty` | Callers have nothing to act on otherwise |
| A2A `Settled`/`Accepted` echoes UETR | `A2aMissingUetrOnAccept` | Settlement notifications match back to UETR; missing UETR breaks reconciliation |
| `catch_unwind` on transport-error paths | `PanicOnTransportError` | A panicking driver brings down the orchestrator thread |
## DeterministicCardAcquirer surface
```rust
DeterministicCardAcquirer::new()
.with_default_status(AuthStatus::Settled)
.with_key_override("k-decline", AuthStatus::HardDecline, Some("nsf".into()))
.with_amount_ge(
Money::from_minor(1_000_000, Currency::USD),
AuthStatus::RequiresCustomerAction,
None,
)
.with_transport_error("connect timeout") // exclusive — short-circuits everything
;
// After running:
acq.auth_history(); // every AuthRequest the driver saw
acq.capture_history(); // every CaptureRequest
acq.refund_history();
acq.void_history();
```
Same shape on `DeterministicA2aGateway` — UETR overrides, amount
rules, transport-error mode, `transfer_history()` /
`query_history()` for assertions.
## Why a runtime harness instead of a trait constraint
Rust's type system enforces the shape of the trait but not its
behavior. "The idempotency key flows through unchanged" and
"transport errors don't panic" are properties only a runtime probe
can verify. A doc-only convention is brittle; the runnable harness
gives driver authors an immediate pass/fail signal that the OpenPay
project can keep in sync with the trait semantics as they evolve.
## End-to-end integration
The integration test suite at
[`tests/orchestrator_integration.rs`](crates/op-driver-sdk/tests/orchestrator_integration.rs)
proves the deterministic drivers flow through a real `Orchestrator`
via `CardAdapter` / `A2aAdapter` with `PolicyRouter`. Operators
copy these tests as the starting point for their own driver
acceptance suite.
## Honest concerns (carry-forward)
- **Idempotency-key propagation isn't probed.** The harness
documents the requirement (the key the caller sends must equal
the key the PSP sees) but can't actively check it without
driver cooperation — there's no way to inspect a third-party
PSP's view of the request. Driver authors should write a
driver-specific test asserting this.
- **No async harness.** All conformance probes are sync, matching
the current sync acquirer traits. When the traits go async we
add the same checks with `tokio::test`.
- **No fuzz / property-based checks.** The harness is a fixed
battery. Adding proptest-driven invariants ("for any input the
driver responds in finite time / never panics") would be a
natural follow-up.
- **`run_card` runs against the real driver.** Calling it from CI
*will* hit the PSP sandbox if the driver does network I/O.
Operators wrap the driver in their own throttle if they want to
run conformance on every PR.
- **No HTTP request recording.** Drivers that talk over HTTP would
benefit from a record/replay layer (capture sandbox responses,
replay deterministically in tests). That's a separate crate.
Most operators handle this with `wiremock` or `mockito`.
## Test totals
```
op-driver-sdk 24 tests
card mock 8 (default / overrides / amount / transport / history / supports)
a2a mock 5 (default / override / amount / transport / name)
conformance unit 6 (harness self-test + failure detection)
orchestrator integration 5 (card happy, card decline, a2a happy, a2a amount, conformance both)
----
+24 net
```
`cargo test --workspace`: **930 passing, 0 failing.**
`cargo build --workspace --all-targets`: **0 warnings.**
`cargo clippy --workspace --all-targets`: **0 warnings.**
## What's next
With Phases 22 → 23 → 24 complete, OpenPay now has:
- A double-entry ledger with bi-temporal time-travel (Phases 12, 17)
- A graph-backed audit / fraud query layer (Phases 14, 18, 19, 21)
- A pluggable orchestrator with routing signals (Phases 11, 18, 19, 20)
- Reconciliation with ISO 20022 sources (Phases 15, 20)
- Refund + dispute workflows (Phase 21)
- Settlement + payout file generation (Phase 22)
- A deployable HTTP API server (Phase 23)
- A driver-author SDK with conformance harness (Phase 24)
The remaining gap to "ship and run in production" is operator-
side: a real merchant directory, a real fraud scorer model, real
PSP integrations using the SDK. None of those belong in the
reference stack — they're per-operator concerns the architecture
deliberately leaves pluggable. **OpenPay is now SOTA-deployable.**