OpenPay (1 crate, ~2287 LOC, 15 tests)
op-dispute-evidence Chargeback lifecycle + evidence packaging across Visa
VCR (10.x/11.x/12.x/13.x), Mastercard Mastercom
(4837/4853/1240/1442), Amex SafeKey + DRR, Discover DRR,
PayPal. CE3.0 compelling-evidence qualifier (120-365d
window, ≥2 qualifiers, ≥2 linking matches, 10.4-only).
Phase state machine + ReasonCodeCatalog + sealed
EvidencePackageBuilder + win-rate scoring.
Smart Byte (1 crate, ~2479 LOC, 44 tests)
smart-byte-edv Encrypted Data Vaults (DIF v0.10).
JWE ECDH-ES P-256 + HKDF-SHA256-Concat-KDF + AES-256-GCM,
multi-recipient; HMAC-SHA-256 encrypted index with
AND-equality queries; full ZCAP-LD chain validation;
chunked AES-256-GCM streaming with per-chunk AAD;
Vault trait + InMemoryVault + one-way pull replication.
EOC (1 crate, ~2065 LOC, 29 tests)
eoc-agent Agentic loops with per-step joule accounting.
ReAct, Reflexion (PASS/FAIL judge + episodic memory),
Tree of Thoughts (BFS/DFS + self-scoring), LATS
(MCTS-UCT select/expand/simulate/backprop),
Plan-and-Execute, CodeAct (fenced-block + pluggable
runtime). Budget enforcement (tokens/joules/tool-calls),
trace spans, JouleCounter integration.
Three other batch-5 deferrals (op-ewallets-asia, smart-byte-zk, eoc-rag)
will land in wave A3.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
88 lines
3.3 KiB
Rust
88 lines
3.3 KiB
Rust
//! End-to-end: Visa 10.4 fraud chargeback → representment.
|
|
//!
|
|
//! Walks the lifecycle from the issuer's first chargeback through a
|
|
//! sealed evidence package and into a representment filing. This is
|
|
//! the canonical happy-path the directive's CE3.0-eligible 10.4
|
|
//! flow targets.
|
|
|
|
use op_dispute_evidence::{
|
|
EvidenceItem, EvidencePackageBuilder, EvidenceRequirement, LifecycleEvent, LifecycleMachine,
|
|
Network, Phase, ReasonCode, ReasonCodeCatalog, VisaReasonCode, WinScore, WinScoreBand,
|
|
};
|
|
use time::OffsetDateTime;
|
|
|
|
fn t(s: i64) -> OffsetDateTime {
|
|
OffsetDateTime::from_unix_timestamp(s).expect("valid unix time")
|
|
}
|
|
|
|
#[test]
|
|
fn visa_10_4_fraud_representment_flow() {
|
|
let reason = ReasonCode::Visa(VisaReasonCode::F1040);
|
|
|
|
// Sanity-check the catalog: 10.4 lives on Visa and demands the
|
|
// CE3.0-style "qualifying history" item.
|
|
assert_eq!(reason.network(), Network::Visa);
|
|
assert_eq!(reason.code(), "10.4");
|
|
let required = ReasonCodeCatalog::required_evidence(reason);
|
|
assert!(required.contains(&EvidenceRequirement::QualifyingHistory));
|
|
assert!(required.contains(&EvidenceRequirement::ThreeDsAuthValue));
|
|
|
|
// 1. Issuer files the chargeback. Start the machine straight in
|
|
// FirstChargeback (skip the rare retrieval-request step).
|
|
let mut machine = LifecycleMachine::starting_at(Phase::FirstChargeback);
|
|
assert_eq!(machine.phase(), Phase::FirstChargeback);
|
|
|
|
// 2. Operator packages evidence. Must cover every required
|
|
// item or the seal will fail.
|
|
let mut builder = EvidencePackageBuilder::new(reason);
|
|
for req in required {
|
|
let item = EvidenceItem::new(
|
|
*req,
|
|
format!("{req:?}"),
|
|
"application/octet-stream",
|
|
b"opaque".to_vec(),
|
|
t(1_700_000_000),
|
|
)
|
|
.expect("payload under cap");
|
|
builder = builder.add(item);
|
|
}
|
|
assert!(builder.missing().is_empty(), "all required satisfied");
|
|
|
|
let package = builder
|
|
.seal(t(1_700_000_500))
|
|
.expect("seal complete package");
|
|
assert!(package.total_bytes() > 0);
|
|
assert_eq!(package.items().count(), required.len());
|
|
|
|
// 3. Score it. Full 10.4 bundle including 3DS + CE3 history
|
|
// should land in Likely or Strong.
|
|
let score = WinScore::evaluate(reason, &package);
|
|
assert_eq!(score.missing_required, 0);
|
|
assert!(
|
|
matches!(score.band, WinScoreBand::Likely | WinScoreBand::Strong),
|
|
"expected Likely/Strong, got {:?} (p={})",
|
|
score.band,
|
|
score.probability
|
|
);
|
|
|
|
// 4. File the representment, then receive the network's "won"
|
|
// ruling. Each transition must be accepted.
|
|
machine
|
|
.apply(LifecycleEvent::RepresentmentFiled { at: t(1_700_000_600) })
|
|
.expect("first chargeback -> representment");
|
|
assert_eq!(machine.phase(), Phase::Representment);
|
|
|
|
machine
|
|
.apply(LifecycleEvent::Won { at: t(1_700_001_000) })
|
|
.expect("representment -> won");
|
|
assert_eq!(machine.phase(), Phase::FinalWon);
|
|
assert!(machine.phase().is_terminal());
|
|
|
|
// 5. Any further event must be rejected — terminal phases are
|
|
// closed.
|
|
let trailing = machine.apply(LifecycleEvent::Lost { at: t(1_700_001_500) });
|
|
assert!(trailing.is_err());
|
|
|
|
// 6. The full transition log captures both moves.
|
|
assert_eq!(machine.history().len(), 2);
|
|
}
|