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.
107 lines
4.1 KiB
Rust
107 lines
4.1 KiB
Rust
//! Cross-store persistence: one `.graph` file holds refund,
|
|
//! dispute, settlement, and idempotency state; reopening the file
|
|
//! recovers everything.
|
|
|
|
use op_core::{Currency, Money};
|
|
use op_dispute::reason::DisputeReason;
|
|
use op_dispute::{Dispute, DisputeStore};
|
|
use op_graph::{
|
|
GraphDisputeStore, GraphHandle, GraphIdempotencyStore, GraphRefundStore, GraphSettlementStore,
|
|
};
|
|
use op_ledger::TransactionId;
|
|
use op_orchestrator::{
|
|
Attempt, AttemptOutcome, IdempotencyKey, IdempotencyStore, OrchestrationOutcome, TerminalStatus,
|
|
};
|
|
use op_refund::reason::RefundReason;
|
|
use op_refund::{Refund, RefundStore};
|
|
use op_settlement::{Batch, PayoutRail, SettlementStore};
|
|
use tempfile::tempdir;
|
|
|
|
#[test]
|
|
fn refund_dispute_settlement_idempotency_all_persist_on_one_file() {
|
|
let dir = tempdir().unwrap();
|
|
let path = dir.path().join("openpay.graph");
|
|
|
|
// Phase 1: write through fresh handle.
|
|
let (refund_id, dispute_id, batch_id, idem_key) = {
|
|
let handle = GraphHandle::new_persistent(&path).unwrap();
|
|
let refunds = GraphRefundStore::with_handle(handle.clone());
|
|
let disputes = GraphDisputeStore::with_handle(handle.clone());
|
|
let settlement = GraphSettlementStore::with_handle(handle.clone());
|
|
let idempotency = GraphIdempotencyStore::with_handle(handle.clone());
|
|
|
|
// Refund.
|
|
let r = Refund::new(
|
|
TransactionId::new(),
|
|
Money::from_minor(500, Currency::USD),
|
|
RefundReason::CustomerRequest,
|
|
1_000,
|
|
)
|
|
.unwrap()
|
|
.with_external_id("ref-persist");
|
|
let refund_id = refunds.create_refund(r).unwrap();
|
|
|
|
// Dispute.
|
|
let d = Dispute::new(
|
|
TransactionId::new(),
|
|
Money::from_minor(700, Currency::USD),
|
|
DisputeReason::Fraudulent,
|
|
2_000,
|
|
)
|
|
.unwrap()
|
|
.with_external_id("disp-persist");
|
|
let dispute_id = disputes.create_dispute(d).unwrap();
|
|
|
|
// Settlement.
|
|
let b = Batch::open(Currency::USD, PayoutRail::AchNacha, 3_000)
|
|
.with_external_id("batch-persist");
|
|
let batch_id = settlement.create_batch(b).unwrap();
|
|
|
|
// Idempotency.
|
|
let key = IdempotencyKey::new("idem-persist");
|
|
let prior = idempotency.reserve(&key, "sig-1");
|
|
assert!(prior.is_none(), "first reserve should be empty");
|
|
idempotency.commit(
|
|
&key,
|
|
&OrchestrationOutcome {
|
|
terminal_status: TerminalStatus::Approved,
|
|
attempts: vec![Attempt {
|
|
rail: op_core::RailKind::Card,
|
|
driver: "test".into(),
|
|
outcome: AttemptOutcome::Success,
|
|
}],
|
|
rail_used: Some(op_core::RailKind::Card),
|
|
psp_payment_id: Some("psp_x".into()),
|
|
uetr: None,
|
|
},
|
|
);
|
|
|
|
handle.compact().unwrap();
|
|
(refund_id, dispute_id, batch_id, key)
|
|
};
|
|
|
|
// Phase 2: drop the original handle, reopen, verify recovery.
|
|
let handle = GraphHandle::new_persistent(&path).unwrap();
|
|
let refunds = GraphRefundStore::with_handle(handle.clone());
|
|
let disputes = GraphDisputeStore::with_handle(handle.clone());
|
|
let settlement = GraphSettlementStore::with_handle(handle.clone());
|
|
let idempotency = GraphIdempotencyStore::with_handle(handle.clone());
|
|
|
|
let r = refunds.get_refund(refund_id).unwrap();
|
|
assert_eq!(r.external_id.as_deref(), Some("ref-persist"));
|
|
assert_eq!(r.amount, Money::from_minor(500, Currency::USD));
|
|
|
|
let d = disputes.get_dispute(dispute_id).unwrap();
|
|
assert_eq!(d.external_id.as_deref(), Some("disp-persist"));
|
|
assert_eq!(d.reason, DisputeReason::Fraudulent);
|
|
|
|
let b = settlement.get_batch(batch_id).unwrap();
|
|
assert_eq!(b.external_id.as_deref(), Some("batch-persist"));
|
|
assert_eq!(b.rail, PayoutRail::AchNacha);
|
|
|
|
let rec = idempotency.reserve(&idem_key, "sig-1").unwrap();
|
|
assert_eq!(rec.body_signature, "sig-1");
|
|
let outcome = rec.outcome.expect("committed outcome should persist");
|
|
assert_eq!(outcome.terminal_status, TerminalStatus::Approved);
|
|
assert_eq!(outcome.psp_payment_id.as_deref(), Some("psp_x"));
|
|
}
|