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.
286 lines
10 KiB
Rust
286 lines
10 KiB
Rust
//! End-to-end integration: wire `DeterministicCardAcquirer` and
|
|
//! `DeterministicA2aGateway` into a real Orchestrator, prove the
|
|
//! sample-driver path works without any live PSP.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use op_core::RailKind;
|
|
use op_core::{CryptoAddress, Currency, Money, PaymentMethod, VaultRef};
|
|
use op_driver_sdk::{
|
|
DeterministicA2aGateway, DeterministicCardAcquirer, DeterministicCryptoGateway, conformance,
|
|
};
|
|
use op_orchestrator::{
|
|
A2aAdapter, CardAdapter, CryptoAdapter, IdempotencyKey, MerchantBankProfile, Orchestrator,
|
|
PaymentIntent, PolicyRouter, TerminalStatus,
|
|
};
|
|
use op_rails_a2a::acquirer::{A2aStatus, ParticipantId};
|
|
use op_rails_card::acquirer::AuthStatus;
|
|
use op_rails_crypto::{CryptoStatus, StableToken};
|
|
|
|
#[test]
|
|
fn deterministic_card_routes_through_orchestrator_and_settles() {
|
|
let acquirer = Arc::new(DeterministicCardAcquirer::new());
|
|
let card = Arc::new(CardAdapter::new("det-card", acquirer.clone()));
|
|
|
|
let mut orch = Orchestrator::new().with_router(Box::new(PolicyRouter::new(
|
|
vec!["det-card".to_owned()],
|
|
vec![],
|
|
)));
|
|
orch.register_adapter(card);
|
|
|
|
let intent = PaymentIntent::new(
|
|
IdempotencyKey::new("e2e-card-1"),
|
|
Money::from_minor(2500, Currency::USD),
|
|
PaymentMethod::Vault(VaultRef::new("tok_v7_e2e")),
|
|
);
|
|
let outcome = orch.run(&intent).unwrap();
|
|
assert_eq!(outcome.terminal_status, TerminalStatus::Approved);
|
|
assert_eq!(outcome.attempt_count(), 1);
|
|
assert!(outcome.psp_payment_id.is_some());
|
|
|
|
// Idempotency key flowed into the acquirer untouched.
|
|
let history = acquirer.auth_history();
|
|
assert_eq!(history.len(), 1);
|
|
assert_eq!(history[0].idempotency_key, "e2e-card-1");
|
|
}
|
|
|
|
#[test]
|
|
fn deterministic_card_with_key_override_declines() {
|
|
let acquirer = Arc::new(DeterministicCardAcquirer::new().with_key_override(
|
|
"e2e-decline",
|
|
AuthStatus::HardDecline,
|
|
Some("nsf".into()),
|
|
));
|
|
let card = Arc::new(CardAdapter::new("det-card", acquirer));
|
|
let mut orch = Orchestrator::new().with_router(Box::new(PolicyRouter::new(
|
|
vec!["det-card".to_owned()],
|
|
vec![],
|
|
)));
|
|
orch.register_adapter(card);
|
|
|
|
let intent = PaymentIntent::new(
|
|
IdempotencyKey::new("e2e-decline"),
|
|
Money::from_minor(2500, Currency::USD),
|
|
PaymentMethod::Vault(VaultRef::new("tok_v7_decline")),
|
|
);
|
|
let outcome = orch.run(&intent).unwrap();
|
|
assert_eq!(outcome.terminal_status, TerminalStatus::Declined);
|
|
}
|
|
|
|
#[test]
|
|
fn deterministic_a2a_routes_through_orchestrator() {
|
|
let gateway = Arc::new(DeterministicA2aGateway::new().with_name("det-a2a"));
|
|
let profile = MerchantBankProfile {
|
|
creditor_agent: ParticipantId::Aba("021000021".into()),
|
|
creditor_account: "1234567890".into(),
|
|
creditor_name: "ACME CORP".into(),
|
|
default_debtor_agent: ParticipantId::Aba("121000248".into()),
|
|
default_debtor_name: "Customer".into(),
|
|
};
|
|
let a2a = Arc::new(A2aAdapter::new("det-a2a", gateway.clone(), profile));
|
|
|
|
let mut orch = Orchestrator::new().with_router(Box::new(PolicyRouter::new(
|
|
vec![],
|
|
vec!["det-a2a".to_owned()],
|
|
)));
|
|
orch.register_adapter(a2a);
|
|
|
|
let intent = PaymentIntent::new(
|
|
IdempotencyKey::new("e2e-a2a-1"),
|
|
Money::from_minor(50_000, Currency::USD),
|
|
PaymentMethod::A2a(op_core::A2aKey::UsAch {
|
|
routing: "021000021".into(),
|
|
account: "9999".into(),
|
|
}),
|
|
);
|
|
let outcome = orch.run(&intent).unwrap();
|
|
assert_eq!(outcome.terminal_status, TerminalStatus::Approved);
|
|
assert!(outcome.uetr.is_some());
|
|
assert_eq!(gateway.transfer_history().len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn deterministic_a2a_with_amount_rule_routes_to_pending() {
|
|
let gateway = Arc::new(
|
|
DeterministicA2aGateway::new()
|
|
.with_name("det-a2a")
|
|
.with_amount_ge(
|
|
Money::from_minor(1_000_000, Currency::USD),
|
|
A2aStatus::Pending,
|
|
None,
|
|
),
|
|
);
|
|
let profile = MerchantBankProfile {
|
|
creditor_agent: ParticipantId::Aba("021000021".into()),
|
|
creditor_account: "1234567890".into(),
|
|
creditor_name: "ACME CORP".into(),
|
|
default_debtor_agent: ParticipantId::Aba("121000248".into()),
|
|
default_debtor_name: "Customer".into(),
|
|
};
|
|
let a2a = Arc::new(A2aAdapter::new("det-a2a", gateway, profile));
|
|
let mut orch = Orchestrator::new().with_router(Box::new(PolicyRouter::new(
|
|
vec![],
|
|
vec!["det-a2a".to_owned()],
|
|
)));
|
|
orch.register_adapter(a2a);
|
|
|
|
let intent = PaymentIntent::new(
|
|
IdempotencyKey::new("e2e-big"),
|
|
Money::from_minor(2_000_000, Currency::USD),
|
|
PaymentMethod::A2a(op_core::A2aKey::UsAch {
|
|
routing: "021000021".into(),
|
|
account: "9999".into(),
|
|
}),
|
|
);
|
|
// Pending → orchestrator treats as soft failure. With no
|
|
// fallback rail this surfaces as either `Declined` or
|
|
// `Err(AllRailsExhausted)` depending on the engine config;
|
|
// both are correct "not approved" signals.
|
|
match orch.run(&intent) {
|
|
Ok(outcome) => {
|
|
assert_ne!(outcome.terminal_status, TerminalStatus::Approved);
|
|
}
|
|
Err(op_orchestrator::Error::AllRailsExhausted { .. }) => {}
|
|
Err(other) => panic!("unexpected error: {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn deterministic_crypto_routes_through_orchestrator() {
|
|
let gateway = Arc::new(
|
|
DeterministicCryptoGateway::for_token(StableToken::UsdcBase).with_name("usdc-base"),
|
|
);
|
|
let adapter = Arc::new(CryptoAdapter::new("usdc-base", gateway.clone()));
|
|
let router =
|
|
PolicyRouter::new(vec![], vec![]).with_crypto_drivers(vec!["usdc-base".to_owned()]);
|
|
let mut orch = Orchestrator::new().with_router(Box::new(router));
|
|
orch.register_adapter(adapter);
|
|
|
|
let intent = PaymentIntent::new(
|
|
IdempotencyKey::new("crypto-e2e-1"),
|
|
Money::from_minor(50_000, Currency::USD),
|
|
PaymentMethod::Crypto(CryptoAddress::new(
|
|
"base",
|
|
"0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
|
|
)),
|
|
);
|
|
let outcome = orch.run(&intent).unwrap();
|
|
assert_eq!(outcome.terminal_status, TerminalStatus::Approved);
|
|
assert!(outcome.psp_payment_id.is_some(), "tx_hash expected");
|
|
let history = gateway.transfer_history();
|
|
assert_eq!(history.len(), 1);
|
|
assert_eq!(history[0].idempotency_key, "crypto-e2e-1");
|
|
}
|
|
|
|
#[test]
|
|
fn crypto_rejected_status_declines_terminally() {
|
|
let gateway = Arc::new(
|
|
DeterministicCryptoGateway::for_token(StableToken::UsdcSolana)
|
|
.with_name("usdc-solana")
|
|
.with_key_override("rev", CryptoStatus::Rejected, Some("revert".into())),
|
|
);
|
|
let adapter = Arc::new(CryptoAdapter::new("usdc-solana", gateway));
|
|
let router =
|
|
PolicyRouter::new(vec![], vec![]).with_crypto_drivers(vec!["usdc-solana".to_owned()]);
|
|
let mut orch = Orchestrator::new().with_router(Box::new(router));
|
|
orch.register_adapter(adapter);
|
|
|
|
let intent = PaymentIntent::new(
|
|
IdempotencyKey::new("rev"),
|
|
Money::from_minor(100, Currency::USD),
|
|
PaymentMethod::Crypto(CryptoAddress::new(
|
|
"solana",
|
|
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
)),
|
|
);
|
|
let outcome = orch.run(&intent).unwrap();
|
|
assert_eq!(outcome.terminal_status, TerminalStatus::Declined);
|
|
}
|
|
|
|
#[test]
|
|
fn three_ds_challenge_then_resume_completes() {
|
|
// Stand up an orchestrator with a deterministic card acquirer
|
|
// that returns RequiresCustomerAction for a specific
|
|
// idempotency key. Then call resume() with the returned
|
|
// psp_payment_id and confirm the final outcome is Approved.
|
|
let acquirer = Arc::new(DeterministicCardAcquirer::new().with_key_override(
|
|
"3ds-pending",
|
|
AuthStatus::RequiresCustomerAction,
|
|
None,
|
|
));
|
|
let card = Arc::new(CardAdapter::new("det-card", acquirer.clone()));
|
|
let mut orch = Orchestrator::new().with_router(Box::new(PolicyRouter::new(
|
|
vec!["det-card".to_owned()],
|
|
vec![],
|
|
)));
|
|
orch.register_adapter(card);
|
|
|
|
let intent = PaymentIntent::new(
|
|
IdempotencyKey::new("3ds-pending"),
|
|
Money::from_minor(15_000, Currency::USD),
|
|
PaymentMethod::Vault(VaultRef::new("tok_v7_3ds")),
|
|
);
|
|
let outcome = orch.run(&intent).unwrap();
|
|
assert_eq!(
|
|
outcome.terminal_status,
|
|
TerminalStatus::RequiresCustomerAction
|
|
);
|
|
let psp_id = outcome.psp_payment_id.expect("psp id on challenge");
|
|
|
|
// Customer completes the challenge out-of-band; operator
|
|
// calls back into the orchestrator with the same intent +
|
|
// psp_payment_id.
|
|
let resumed = orch
|
|
.resume(&intent, RailKind::Card, "det-card", &psp_id)
|
|
.unwrap();
|
|
assert_eq!(resumed.terminal_status, TerminalStatus::Approved);
|
|
assert_eq!(resumed.psp_payment_id.as_deref(), Some(psp_id.as_str()));
|
|
}
|
|
|
|
#[test]
|
|
fn resume_against_unknown_driver_errors() {
|
|
let acquirer = Arc::new(DeterministicCardAcquirer::new());
|
|
let card = Arc::new(CardAdapter::new("det-card", acquirer));
|
|
let mut orch = Orchestrator::new().with_router(Box::new(PolicyRouter::new(
|
|
vec!["det-card".to_owned()],
|
|
vec![],
|
|
)));
|
|
orch.register_adapter(card);
|
|
|
|
let intent = PaymentIntent::new(
|
|
IdempotencyKey::new("missing"),
|
|
Money::from_minor(100, Currency::USD),
|
|
PaymentMethod::Vault(VaultRef::new("tok")),
|
|
);
|
|
let err = orch
|
|
.resume(&intent, RailKind::Card, "no-such-driver", "psp-x")
|
|
.unwrap_err();
|
|
assert!(matches!(err, op_orchestrator::Error::NoEligibleRail { .. }));
|
|
}
|
|
|
|
#[test]
|
|
fn all_three_deterministic_drivers_pass_conformance() {
|
|
let card = DeterministicCardAcquirer::new();
|
|
let card_report = conformance::run_card(&card);
|
|
assert!(
|
|
card_report.is_clean(),
|
|
"card conformance failed: {:?}",
|
|
card_report.failures
|
|
);
|
|
|
|
let a2a = DeterministicA2aGateway::new();
|
|
let a2a_report = conformance::run_a2a(&a2a);
|
|
assert!(
|
|
a2a_report.is_clean(),
|
|
"a2a conformance failed: {:?}",
|
|
a2a_report.failures
|
|
);
|
|
|
|
let crypto = DeterministicCryptoGateway::new();
|
|
let crypto_report = conformance::run_crypto(&crypto);
|
|
assert!(
|
|
crypto_report.is_clean(),
|
|
"crypto conformance failed: {:?}",
|
|
crypto_report.failures
|
|
);
|
|
}
|