Skip to main content

chio_kernel/payment/
sim.rs

1//! Deterministic, no-broadcast payment adapter for sim-first acceptance lanes.
2//!
3//! Custody-neutral by construction: it performs no HTTP, holds no key, and moves
4//! no funds. Authorization ids are a pure function of the request so smokes are
5//! reproducible. It echoes the governed binding into `metadata` so the settled
6//! tool-call receipt carries the governed intent hash and approval token id.
7
8use crate::payment::{
9    PaymentAdapter, PaymentAuthorization, PaymentAuthorizationState, PaymentAuthorizeRequest,
10    PaymentError, PaymentResult, RailSettlementStatus,
11};
12
13/// Deterministic no-broadcast payment adapter.
14#[derive(Debug, Clone, Default)]
15pub struct SimPaymentAdapter;
16
17impl SimPaymentAdapter {
18    #[must_use]
19    pub fn new() -> Self {
20        Self
21    }
22
23    fn deterministic_id(
24        prefix: &str,
25        reference: &str,
26        amount_units: u64,
27        currency: &str,
28    ) -> String {
29        let seed = format!("{reference}|{amount_units}|{currency}");
30        let digest = chio_core::sha256_hex(seed.as_bytes());
31        format!("{prefix}-{}", &digest[..32])
32    }
33}
34
35impl PaymentAdapter for SimPaymentAdapter {
36    fn authorize(
37        &self,
38        request: &PaymentAuthorizeRequest,
39    ) -> Result<PaymentAuthorization, PaymentError> {
40        let authorization_id = Self::deterministic_id(
41            "sim",
42            &request.reference,
43            request.amount_units,
44            &request.currency,
45        );
46        Ok(PaymentAuthorization {
47            authorization_id,
48            state: PaymentAuthorizationState::PrepaidFinal,
49            metadata: serde_json::json!({
50                "adapter": "sim",
51                "mode": "prepaid_no_broadcast",
52                "governed": request.governed,
53                "commerce": request.commerce,
54            }),
55        })
56    }
57
58    fn capture(
59        &self,
60        authorization_id: &str,
61        _amount_units: u64,
62        _currency: &str,
63        _reference: &str,
64    ) -> Result<PaymentResult, PaymentError> {
65        Ok(PaymentResult {
66            transaction_id: authorization_id.to_string(),
67            settlement_status: RailSettlementStatus::Settled,
68            metadata: serde_json::json!({ "adapter": "sim", "action": "capture" }),
69        })
70    }
71
72    fn release(
73        &self,
74        authorization_id: &str,
75        _reference: &str,
76    ) -> Result<PaymentResult, PaymentError> {
77        Ok(PaymentResult {
78            transaction_id: authorization_id.to_string(),
79            settlement_status: RailSettlementStatus::Released,
80            metadata: serde_json::json!({ "adapter": "sim", "action": "release" }),
81        })
82    }
83
84    fn refund(
85        &self,
86        transaction_id: &str,
87        _amount_units: u64,
88        _currency: &str,
89        _reference: &str,
90    ) -> Result<PaymentResult, PaymentError> {
91        Ok(PaymentResult {
92            transaction_id: transaction_id.to_string(),
93            settlement_status: RailSettlementStatus::Refunded,
94            metadata: serde_json::json!({ "adapter": "sim", "action": "refund" }),
95        })
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    fn request(reference: &str, amount: u64) -> PaymentAuthorizeRequest {
104        PaymentAuthorizeRequest {
105            amount_units: amount,
106            currency: "USD".to_string(),
107            payer: "agent-1".to_string(),
108            payee: "srv-1".to_string(),
109            reference: reference.to_string(),
110            governed: None,
111            commerce: None,
112        }
113    }
114
115    #[test]
116    fn authorize_is_deterministic_and_prepaid() {
117        let adapter = SimPaymentAdapter::new();
118        let a = adapter.authorize(&request("req-1", 100)).unwrap();
119        let b = adapter.authorize(&request("req-1", 100)).unwrap();
120        assert_eq!(a.authorization_id, b.authorization_id);
121        assert!(a.authorization_id.starts_with("sim-"));
122        assert!(a.state.is_final(), "sim authorization is prepaid and final");
123    }
124
125    #[test]
126    fn distinct_requests_get_distinct_ids() {
127        let adapter = SimPaymentAdapter::new();
128        let a = adapter.authorize(&request("req-1", 100)).unwrap();
129        let c = adapter.authorize(&request("req-2", 100)).unwrap();
130        assert_ne!(a.authorization_id, c.authorization_id);
131    }
132
133    #[test]
134    fn capture_and_release_map_to_settled_and_released() {
135        let adapter = SimPaymentAdapter::new();
136        let captured = adapter.capture("sim-abc", 100, "USD", "req-1").unwrap();
137        assert_eq!(captured.settlement_status, RailSettlementStatus::Settled);
138        let released = adapter.release("sim-abc", "req-1").unwrap();
139        assert_eq!(released.settlement_status, RailSettlementStatus::Released);
140    }
141}