1use compact_str::CompactString;
4use r402_protocol::error::ErrorReason;
5use r402_protocol::payment::{PaymentRequirements, SettleResponse};
6
7use super::policy::HookPolicyError;
8
9#[must_use]
11pub fn snapshot_payment_requirements_list(
12 requirements: &[PaymentRequirements],
13) -> Vec<PaymentRequirements> {
14 requirements.to_vec()
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct SettleResponseCoreSnapshot {
20 pub success: bool,
22 pub transaction: CompactString,
24 pub network: CompactString,
26 pub amount: Option<CompactString>,
28 pub payer: Option<CompactString>,
30 pub error_reason: Option<ErrorReason>,
32 pub error_message: Option<CompactString>,
34}
35
36#[must_use]
38pub fn snapshot_settle_response_core(result: &SettleResponse) -> SettleResponseCoreSnapshot {
39 match result {
40 SettleResponse::Success {
41 payer,
42 transaction,
43 network,
44 amount,
45 ..
46 } => SettleResponseCoreSnapshot {
47 success: true,
48 transaction: transaction.clone(),
49 network: network.clone(),
50 amount: amount.clone(),
51 payer: payer.clone(),
52 error_reason: None,
53 error_message: None,
54 },
55 SettleResponse::Failure {
56 reason,
57 message,
58 payer,
59 transaction,
60 network,
61 ..
62 } => SettleResponseCoreSnapshot {
63 success: false,
64 transaction: transaction.clone(),
65 network: network.clone(),
66 amount: None,
67 payer: payer.clone(),
68 error_reason: Some(reason.clone()),
69 error_message: message.clone(),
70 },
71 _ => SettleResponseCoreSnapshot {
72 success: result.is_success(),
73 transaction: CompactString::default(),
74 network: CompactString::default(),
75 amount: None,
76 payer: None,
77 error_reason: None,
78 error_message: None,
79 },
80 }
81}
82
83pub fn assert_settle_response_core_unchanged(
89 before: &SettleResponseCoreSnapshot,
90 after: &SettleResponse,
91 extension_key: &str,
92) -> Result<(), HookPolicyError> {
93 let after_snap = snapshot_settle_response_core(after);
94 assert_settle_field(
95 "success",
96 before.success == after_snap.success,
97 extension_key,
98 )?;
99 assert_settle_field(
100 "transaction",
101 before.transaction == after_snap.transaction,
102 extension_key,
103 )?;
104 assert_settle_field(
105 "network",
106 before.network == after_snap.network,
107 extension_key,
108 )?;
109 assert_settle_field("amount", before.amount == after_snap.amount, extension_key)?;
110 assert_settle_field("payer", before.payer == after_snap.payer, extension_key)?;
111 assert_settle_field(
112 "errorReason",
113 before.error_reason == after_snap.error_reason,
114 extension_key,
115 )?;
116 assert_settle_field(
117 "errorMessage",
118 before.error_message == after_snap.error_message,
119 extension_key,
120 )
121}
122
123fn assert_settle_field(
124 field: &str,
125 equal: bool,
126 extension_key: &str,
127) -> Result<(), HookPolicyError> {
128 if equal {
129 Ok(())
130 } else {
131 Err(HookPolicyError::settle_extension(
132 extension_key,
133 &format!("field \"{field}\" is immutable after facilitator settle"),
134 ))
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use r402_protocol::payment::Extensions;
141
142 use super::*;
143
144 fn success_settle() -> SettleResponse {
145 SettleResponse::Success {
146 payer: Some("0xpayer".into()),
147 transaction: "0xtx".into(),
148 network: "eip155:8453".into(),
149 amount: None,
150 extensions: Extensions::new(),
151 extension_responses: Extensions::new(),
152 extra: None,
153 }
154 }
155
156 #[test]
157 fn settle_core_unchanged_allows_extension_map_edits() {
158 let mut base = success_settle();
159 let snap = snapshot_settle_response_core(&base);
160 if let SettleResponse::Success {
161 ref mut extensions, ..
162 } = base
163 {
164 extensions.insert(
165 "a",
166 r402_protocol::payment::ExtensionEntry::info(serde_json::json!(1)),
167 );
168 }
169 assert!(assert_settle_response_core_unchanged(&snap, &base, "ext").is_ok());
170 }
171
172 #[test]
173 fn settle_core_unchanged_rejects_transaction_change() {
174 let mut base = success_settle();
175 let snap = snapshot_settle_response_core(&base);
176 if let SettleResponse::Success {
177 ref mut transaction,
178 ..
179 } = base
180 {
181 *transaction = "0xother".into();
182 }
183 let err = assert_settle_response_core_unchanged(&snap, &base, "ext").unwrap_err();
184 assert!(err.to_string().contains("transaction"), "{err}");
185 }
186}