1use compact_str::CompactString;
10use serde::{Deserialize, Serialize};
11
12use super::{Base64Bytes, Extensions};
13use crate::error::FacilitatorError;
14use crate::error_reason::{AsPaymentProblem, ErrorReason};
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(into = "VerifyResponseWire", try_from = "VerifyResponseWire")]
22#[non_exhaustive]
23pub enum VerifyResponse {
24 Valid {
26 payer: CompactString,
28 extensions: Extensions,
30 },
31 Invalid {
33 reason: ErrorReason,
35 message: Option<CompactString>,
37 payer: Option<CompactString>,
39 extensions: Extensions,
41 },
42}
43
44impl VerifyResponse {
45 #[must_use]
47 pub fn valid(payer: impl Into<CompactString>) -> Self {
48 Self::Valid {
49 payer: payer.into(),
50 extensions: Extensions::new(),
51 }
52 }
53
54 #[must_use]
56 pub fn invalid(payer: Option<CompactString>, reason: ErrorReason) -> Self {
57 Self::Invalid {
58 reason,
59 message: None,
60 payer,
61 extensions: Extensions::new(),
62 }
63 }
64
65 #[must_use]
67 pub fn invalid_with_message(
68 payer: Option<CompactString>,
69 reason: ErrorReason,
70 message: impl Into<CompactString>,
71 ) -> Self {
72 Self::Invalid {
73 reason,
74 message: Some(message.into()),
75 payer,
76 extensions: Extensions::new(),
77 }
78 }
79
80 #[must_use]
82 pub const fn is_valid(&self) -> bool {
83 matches!(self, Self::Valid { .. })
84 }
85
86 #[must_use]
89 pub fn from_facilitator_error(error: &FacilitatorError) -> Self {
90 let problem = error.as_payment_problem();
91 Self::Invalid {
92 reason: problem.reason(),
93 message: Some(CompactString::from(problem.details())),
94 payer: None,
95 extensions: Extensions::new(),
96 }
97 }
98}
99
100#[derive(Serialize, Deserialize)]
102#[serde(rename_all = "camelCase", deny_unknown_fields)]
103struct VerifyResponseWire {
104 is_valid: bool,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
106 payer: Option<CompactString>,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 invalid_reason: Option<ErrorReason>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
110 invalid_message: Option<CompactString>,
111 #[serde(default, skip_serializing_if = "Extensions::is_empty")]
112 extensions: Extensions,
113}
114
115impl From<VerifyResponse> for VerifyResponseWire {
116 fn from(value: VerifyResponse) -> Self {
117 match value {
118 VerifyResponse::Valid { payer, extensions } => Self {
119 is_valid: true,
120 payer: Some(payer),
121 invalid_reason: None,
122 invalid_message: None,
123 extensions,
124 },
125 VerifyResponse::Invalid {
126 reason,
127 message,
128 payer,
129 extensions,
130 } => Self {
131 is_valid: false,
132 payer,
133 invalid_reason: Some(reason),
134 invalid_message: message,
135 extensions,
136 },
137 }
138 }
139}
140
141impl TryFrom<VerifyResponseWire> for VerifyResponse {
142 type Error = String;
143 fn try_from(wire: VerifyResponseWire) -> Result<Self, Self::Error> {
144 if wire.is_valid {
145 Ok(Self::Valid {
146 payer: wire.payer.ok_or("missing field: payer")?,
147 extensions: wire.extensions,
148 })
149 } else {
150 Ok(Self::Invalid {
151 reason: wire.invalid_reason.ok_or("missing field: invalidReason")?,
152 message: wire.invalid_message,
153 payer: wire.payer,
154 extensions: wire.extensions,
155 })
156 }
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162#[serde(into = "SettleResponseWire", try_from = "SettleResponseWire")]
163#[non_exhaustive]
164pub enum SettleResponse {
165 Success {
167 payer: CompactString,
169 transaction: CompactString,
171 network: CompactString,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
178 amount: Option<CompactString>,
179 extensions: Extensions,
181 },
182 Failure {
184 reason: ErrorReason,
186 message: Option<CompactString>,
188 payer: Option<CompactString>,
190 network: CompactString,
192 extensions: Extensions,
194 },
195}
196
197impl SettleResponse {
198 #[must_use]
200 pub const fn is_success(&self) -> bool {
201 matches!(self, Self::Success { .. })
202 }
203
204 #[must_use]
213 pub fn encode_base64(&self) -> Option<Base64Bytes> {
214 if !self.is_success() {
215 return None;
216 }
217 let json = serde_json::to_vec(self).ok()?;
218 Some(Base64Bytes::encode(json))
219 }
220
221 #[must_use]
230 pub fn encode_base64_any(&self) -> Option<Base64Bytes> {
231 let json = serde_json::to_vec(self).ok()?;
232 Some(Base64Bytes::encode(json))
233 }
234
235 #[must_use]
237 pub fn from_facilitator_error(
238 error: &FacilitatorError,
239 network: impl Into<CompactString>,
240 ) -> Self {
241 let problem = error.as_payment_problem();
242 Self::Failure {
243 reason: problem.reason(),
244 message: Some(CompactString::from(problem.details())),
245 payer: None,
246 network: network.into(),
247 extensions: Extensions::new(),
248 }
249 }
250}
251
252#[derive(Serialize, Deserialize)]
254#[serde(rename_all = "camelCase", deny_unknown_fields)]
255struct SettleResponseWire {
256 success: bool,
257 #[serde(default, skip_serializing_if = "Option::is_none")]
258 error_reason: Option<ErrorReason>,
259 #[serde(default, skip_serializing_if = "Option::is_none")]
260 error_message: Option<CompactString>,
261 #[serde(default, skip_serializing_if = "Option::is_none")]
262 payer: Option<CompactString>,
263 #[serde(default)]
267 transaction: CompactString,
268 network: CompactString,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
270 amount: Option<CompactString>,
271 #[serde(default, skip_serializing_if = "Extensions::is_empty")]
272 extensions: Extensions,
273}
274
275impl From<SettleResponse> for SettleResponseWire {
276 fn from(value: SettleResponse) -> Self {
277 match value {
278 SettleResponse::Success {
279 payer,
280 transaction,
281 network,
282 amount,
283 extensions,
284 } => Self {
285 success: true,
286 error_reason: None,
287 error_message: None,
288 payer: Some(payer),
289 transaction,
290 network,
291 amount,
292 extensions,
293 },
294 SettleResponse::Failure {
295 reason,
296 message,
297 payer,
298 network,
299 extensions,
300 } => Self {
301 success: false,
302 error_reason: Some(reason),
303 error_message: message,
304 payer,
305 transaction: CompactString::default(),
306 network,
307 amount: None,
308 extensions,
309 },
310 }
311 }
312}
313
314impl TryFrom<SettleResponseWire> for SettleResponse {
315 type Error = String;
316 fn try_from(wire: SettleResponseWire) -> Result<Self, Self::Error> {
317 if wire.success {
318 let payer = wire.payer.ok_or("missing field: payer")?;
319 if wire.transaction.is_empty() {
320 return Err("missing field: transaction".to_owned());
321 }
322 Ok(Self::Success {
323 payer,
324 transaction: wire.transaction,
325 network: wire.network,
326 amount: wire.amount,
327 extensions: wire.extensions,
328 })
329 } else {
330 Ok(Self::Failure {
331 reason: wire.error_reason.ok_or("missing field: errorReason")?,
332 message: wire.error_message,
333 payer: wire.payer,
334 network: wire.network,
335 extensions: wire.extensions,
336 })
337 }
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use serde_json::json;
344
345 use super::*;
346 use crate::wire::ExtensionEntry;
347
348 #[test]
349 fn verify_valid_roundtrip() {
350 let response = VerifyResponse::valid("0xABC");
351 let encoded = serde_json::to_value(&response).unwrap();
352 assert_eq!(encoded["isValid"], true);
353 assert_eq!(encoded["payer"], "0xABC");
354 assert!(encoded.get("invalidReason").is_none());
355
356 let back: VerifyResponse = serde_json::from_value(encoded).unwrap();
357 assert_eq!(back, response);
358 }
359
360 #[test]
361 fn verify_valid_with_extensions_roundtrip() {
362 let mut extensions = Extensions::new();
363 extensions.insert(
364 "payment-identifier",
365 ExtensionEntry::info(json!({"id": "order-123"})),
366 );
367 let response = VerifyResponse::Valid {
368 payer: "0xABC".into(),
369 extensions,
370 };
371 let encoded = serde_json::to_value(&response).unwrap();
372 assert_eq!(
373 encoded["extensions"]["payment-identifier"]["info"]["id"],
374 "order-123"
375 );
376 let back: VerifyResponse = serde_json::from_value(encoded).unwrap();
377 assert_eq!(back, response);
378 }
379
380 #[test]
381 fn verify_invalid_roundtrip() {
382 let response = VerifyResponse::invalid_with_message(
383 Some("0xDEF".into()),
384 ErrorReason::InsufficientFunds,
385 "not enough USDC",
386 );
387 let encoded = serde_json::to_value(&response).unwrap();
388 assert_eq!(encoded["isValid"], false);
389 assert_eq!(encoded["invalidReason"], "insufficient_funds");
390 assert_eq!(encoded["invalidMessage"], "not enough USDC");
391 let back: VerifyResponse = serde_json::from_value(encoded).unwrap();
392 assert_eq!(back, response);
393 }
394
395 #[test]
396 fn settle_success_with_amount_roundtrip() {
397 let response = SettleResponse::Success {
398 payer: "0xABC".into(),
399 transaction: "0xTX".into(),
400 network: "eip155:8453".into(),
401 amount: Some("1000000".into()),
402 extensions: Extensions::new(),
403 };
404 let encoded = serde_json::to_value(&response).unwrap();
405 assert_eq!(encoded["success"], true);
406 assert_eq!(encoded["amount"], "1000000");
407 let back: SettleResponse = serde_json::from_value(encoded).unwrap();
408 assert_eq!(back, response);
409 }
410
411 #[test]
412 fn settle_success_without_amount_is_valid() {
413 let response = SettleResponse::Success {
414 payer: "0xABC".into(),
415 transaction: "0xTX".into(),
416 network: "eip155:1".into(),
417 amount: None,
418 extensions: Extensions::new(),
419 };
420 let encoded = serde_json::to_value(&response).unwrap();
421 assert!(encoded.get("amount").is_none());
422 let back: SettleResponse = serde_json::from_value(encoded).unwrap();
423 assert_eq!(back, response);
424 }
425
426 #[test]
427 fn settle_success_missing_transaction_rejects() {
428 let json = json!({
429 "success": true,
430 "payer": "0xABC",
431 "transaction": "",
432 "network": "eip155:1"
433 });
434 assert!(serde_json::from_value::<SettleResponse>(json).is_err());
435 }
436
437 #[test]
438 fn settle_failure_roundtrip() {
439 let response = SettleResponse::Failure {
440 reason: ErrorReason::DuplicateSettlement,
441 message: Some("already processed".into()),
442 payer: None,
443 network: "solana:mainnet".into(),
444 extensions: Extensions::new(),
445 };
446 let encoded = serde_json::to_value(&response).unwrap();
447 assert_eq!(encoded["errorReason"], "duplicate_settlement");
448 let back: SettleResponse = serde_json::from_value(encoded).unwrap();
449 assert_eq!(back, response);
450 }
451
452 #[test]
455 fn settle_failure_serializes_empty_transaction() {
456 let response = SettleResponse::Failure {
457 reason: ErrorReason::UnexpectedSettleError,
458 message: None,
459 payer: None,
460 network: "eip155:8453".into(),
461 extensions: Extensions::new(),
462 };
463 let encoded = serde_json::to_value(&response).unwrap();
464 assert_eq!(
465 encoded["transaction"], "",
466 "spec §5.3.2 requires `transaction` field present (empty on failure)"
467 );
468 }
469
470 #[test]
471 fn settle_encode_base64_only_for_success() {
472 let success = SettleResponse::Success {
473 payer: "0xA".into(),
474 transaction: "0xT".into(),
475 network: "eip155:1".into(),
476 amount: None,
477 extensions: Extensions::new(),
478 };
479 assert!(success.encode_base64().is_some());
480
481 let failure = SettleResponse::Failure {
482 reason: ErrorReason::UnexpectedSettleError,
483 message: None,
484 payer: None,
485 network: "eip155:1".into(),
486 extensions: Extensions::new(),
487 };
488 assert!(failure.encode_base64().is_none());
489 }
490}