1use serde::{Deserialize, Deserializer, Serialize};
18use thiserror::Error;
19
20use chio_core::{capability::scope::MonetaryAmount, hashing::sha256};
21
22use crate::SettlementError;
23
24pub const SETTLEMENT_OBSERVATION_SCHEMA: &str = "chio.settle.observation.v1";
26
27pub const SETTLEMENT_OUTCOME_SCHEMA: &str = "chio.settle.outcome.v1";
29
30fn deserialize_schema<'de, D>(
31 deserializer: D,
32 expected: &'static str,
33 error: &'static str,
34) -> Result<String, D::Error>
35where
36 D: Deserializer<'de>,
37{
38 let schema = String::deserialize(deserializer)?;
39 if schema == expected {
40 Ok(schema)
41 } else {
42 Err(serde::de::Error::custom(error))
43 }
44}
45
46fn deserialize_observation_schema<'de, D>(deserializer: D) -> Result<String, D::Error>
47where
48 D: Deserializer<'de>,
49{
50 deserialize_schema(
51 deserializer,
52 SETTLEMENT_OBSERVATION_SCHEMA,
53 "unsupported settlement observation schema",
54 )
55}
56
57fn deserialize_outcome_schema<'de, D>(deserializer: D) -> Result<String, D::Error>
58where
59 D: Deserializer<'de>,
60{
61 deserialize_schema(
62 deserializer,
63 SETTLEMENT_OUTCOME_SCHEMA,
64 "unsupported settlement outcome schema",
65 )
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
78#[serde(deny_unknown_fields)]
79pub struct SettlementObservation {
80 #[serde(deserialize_with = "deserialize_observation_schema")]
82 pub schema: String,
83 pub receipt_id: String,
85 pub finalized_at: u64,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub tenant_id: Option<String>,
91 pub tool_server: String,
93 pub tool_name: String,
95 pub capability_id: String,
97 pub amount: MonetaryAmount,
101 pub content_hash: String,
105 pub policy_hash: String,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct SettlementIdempotencyKey {
112 pub receipt_id: String,
114 pub row_version: u64,
116}
117
118impl SettlementObservation {
119 #[must_use]
121 #[allow(clippy::too_many_arguments)]
122 pub fn new(
123 receipt_id: impl Into<String>,
124 finalized_at: u64,
125 tool_server: impl Into<String>,
126 tool_name: impl Into<String>,
127 capability_id: impl Into<String>,
128 amount: MonetaryAmount,
129 content_hash: impl Into<String>,
130 policy_hash: impl Into<String>,
131 ) -> Self {
132 Self {
133 schema: SETTLEMENT_OBSERVATION_SCHEMA.to_string(),
134 receipt_id: receipt_id.into(),
135 finalized_at,
136 tenant_id: None,
137 tool_server: tool_server.into(),
138 tool_name: tool_name.into(),
139 capability_id: capability_id.into(),
140 amount,
141 content_hash: content_hash.into(),
142 policy_hash: policy_hash.into(),
143 }
144 }
145
146 #[must_use]
149 pub fn with_tenant(mut self, tenant_id: impl Into<String>) -> Self {
150 self.tenant_id = Some(tenant_id.into());
151 self
152 }
153
154 #[must_use]
157 pub fn ordering_key(&self) -> (u64, &str) {
158 (self.finalized_at, self.receipt_id.as_str())
159 }
160}
161
162#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
164#[serde(rename_all = "snake_case")]
165pub enum SettlementSkipReason {
166 Denied,
168 NoEconomicIntent,
170 Channelized,
171 ZeroCharge,
173}
174
175#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
177#[serde(rename_all = "snake_case")]
178pub enum SettlementFailureClass {
179 Retryable,
181 Permanent,
183}
184
185#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
187#[serde(rename_all = "snake_case")]
188pub enum SettlementFailureCode {
189 InvalidReceiptSignature,
190 InvalidActionHash,
191 UntrustedReceiptSigner,
192 MalformedFinancialMetadata,
193 InvalidObservation,
194 Rpc,
195 InvalidInput,
196 InvalidDispatch,
197 InvalidBinding,
198 Unsupported,
199 Serialization,
200 Signature,
201 Verification,
202 Backend,
203}
204
205#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
207#[error("unknown settlement failure code")]
208pub struct SettlementFailureCodeParseError;
209
210impl SettlementFailureCode {
211 #[must_use]
213 pub const fn as_str(self) -> &'static str {
214 match self {
215 Self::InvalidReceiptSignature => "invalid_receipt_signature",
216 Self::InvalidActionHash => "invalid_action_hash",
217 Self::UntrustedReceiptSigner => "untrusted_receipt_signer",
218 Self::MalformedFinancialMetadata => "malformed_financial_metadata",
219 Self::InvalidObservation => "invalid_observation",
220 Self::Rpc => "rpc",
221 Self::InvalidInput => "invalid_input",
222 Self::InvalidDispatch => "invalid_dispatch",
223 Self::InvalidBinding => "invalid_binding",
224 Self::Unsupported => "unsupported",
225 Self::Serialization => "serialization",
226 Self::Signature => "signature",
227 Self::Verification => "verification",
228 Self::Backend => "backend",
229 }
230 }
231
232 const fn allows_retry(self) -> bool {
233 matches!(self, Self::Rpc | Self::Backend)
234 }
235}
236
237impl TryFrom<&str> for SettlementFailureCode {
238 type Error = SettlementFailureCodeParseError;
239
240 fn try_from(value: &str) -> Result<Self, Self::Error> {
241 match value {
242 "invalid_receipt_signature" => Ok(Self::InvalidReceiptSignature),
243 "invalid_action_hash" => Ok(Self::InvalidActionHash),
244 "untrusted_receipt_signer" => Ok(Self::UntrustedReceiptSigner),
245 "malformed_financial_metadata" => Ok(Self::MalformedFinancialMetadata),
246 "invalid_observation" => Ok(Self::InvalidObservation),
247 "rpc" => Ok(Self::Rpc),
248 "invalid_input" => Ok(Self::InvalidInput),
249 "invalid_dispatch" => Ok(Self::InvalidDispatch),
250 "invalid_binding" => Ok(Self::InvalidBinding),
251 "unsupported" => Ok(Self::Unsupported),
252 "serialization" => Ok(Self::Serialization),
253 "signature" => Ok(Self::Signature),
254 "verification" => Ok(Self::Verification),
255 "backend" => Ok(Self::Backend),
256 _ => Err(SettlementFailureCodeParseError),
257 }
258 }
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
263#[serde(deny_unknown_fields)]
264pub struct SettlementFailureReason {
265 code: SettlementFailureCode,
266 detail_sha256: [u8; 32],
267}
268
269impl SettlementFailureReason {
270 #[must_use]
272 pub fn from_detail(code: SettlementFailureCode, detail: impl AsRef<[u8]>) -> Self {
273 Self::from_digest(code, *sha256(detail.as_ref()).as_bytes())
274 }
275
276 #[must_use]
278 pub const fn from_digest(code: SettlementFailureCode, detail_sha256: [u8; 32]) -> Self {
279 Self {
280 code,
281 detail_sha256,
282 }
283 }
284
285 #[must_use]
287 pub const fn code(&self) -> SettlementFailureCode {
288 self.code
289 }
290
291 #[must_use]
293 pub const fn detail_sha256(&self) -> &[u8; 32] {
294 &self.detail_sha256
295 }
296
297 #[must_use]
299 pub const fn effective_class(
300 &self,
301 requested: SettlementFailureClass,
302 ) -> SettlementFailureClass {
303 match requested {
304 SettlementFailureClass::Retryable if self.code.allows_retry() => {
305 SettlementFailureClass::Retryable
306 }
307 SettlementFailureClass::Retryable | SettlementFailureClass::Permanent => {
308 SettlementFailureClass::Permanent
309 }
310 }
311 }
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
316#[serde(rename_all = "snake_case", tag = "kind", deny_unknown_fields)]
317pub enum SettlementOutcome {
318 Accepted {
323 #[serde(deserialize_with = "deserialize_outcome_schema")]
325 schema: String,
326 transcript_id: String,
328 },
329 Skipped {
331 #[serde(deserialize_with = "deserialize_outcome_schema")]
333 schema: String,
334 reason: SettlementSkipReason,
336 },
337 Retryable {
339 #[serde(deserialize_with = "deserialize_outcome_schema")]
341 schema: String,
342 reason: SettlementFailureReason,
344 },
345 Permanent {
347 #[serde(deserialize_with = "deserialize_outcome_schema")]
349 schema: String,
350 reason: SettlementFailureReason,
352 },
353}
354
355impl SettlementOutcome {
356 #[must_use]
358 pub fn has_supported_schema(&self) -> bool {
359 let schema = match self {
360 Self::Accepted { schema, .. }
361 | Self::Skipped { schema, .. }
362 | Self::Retryable { schema, .. }
363 | Self::Permanent { schema, .. } => schema,
364 };
365 schema == SETTLEMENT_OUTCOME_SCHEMA
366 }
367
368 #[must_use]
370 pub fn accepted(transcript_id: impl Into<String>) -> Self {
371 Self::Accepted {
372 schema: SETTLEMENT_OUTCOME_SCHEMA.to_string(),
373 transcript_id: transcript_id.into(),
374 }
375 }
376
377 #[must_use]
379 pub fn skipped(reason: SettlementSkipReason) -> Self {
380 Self::Skipped {
381 schema: SETTLEMENT_OUTCOME_SCHEMA.to_string(),
382 reason,
383 }
384 }
385
386 #[must_use]
390 pub fn retryable(reason: SettlementFailureReason) -> Self {
391 match reason.effective_class(SettlementFailureClass::Retryable) {
392 SettlementFailureClass::Retryable => Self::Retryable {
393 schema: SETTLEMENT_OUTCOME_SCHEMA.to_string(),
394 reason,
395 },
396 SettlementFailureClass::Permanent => Self::permanent(reason),
397 }
398 }
399
400 #[must_use]
402 pub fn permanent(reason: SettlementFailureReason) -> Self {
403 Self::Permanent {
404 schema: SETTLEMENT_OUTCOME_SCHEMA.to_string(),
405 reason,
406 }
407 }
408
409 #[must_use]
411 pub fn is_retryable(&self) -> bool {
412 matches!(
413 self,
414 Self::Retryable { reason, .. }
415 if reason.effective_class(SettlementFailureClass::Retryable)
416 == SettlementFailureClass::Retryable
417 )
418 }
419
420 #[must_use]
423 pub fn is_permanent(&self) -> bool {
424 match self {
425 Self::Permanent { .. } => true,
426 Self::Retryable { reason, .. } => {
427 reason.effective_class(SettlementFailureClass::Retryable)
428 == SettlementFailureClass::Permanent
429 }
430 Self::Accepted { .. } | Self::Skipped { .. } => false,
431 }
432 }
433}
434
435#[derive(Debug, Error)]
439pub enum SettlementHookError {
440 #[error("invalid observation: {0}")]
442 InvalidObservation(String),
443 #[error("transient settlement failure: {0}")]
448 Transient(String),
449 #[error("permanent settlement failure: {0}")]
451 Permanent(String),
452 #[error("settlement pipeline error: {0}")]
454 Pipeline(#[from] SettlementError),
455}
456
457impl SettlementHookError {
458 #[must_use]
460 pub fn classification(&self) -> (SettlementFailureClass, SettlementFailureReason) {
461 let (class, code, detail) = match self {
462 Self::InvalidObservation(detail) => (
463 SettlementFailureClass::Permanent,
464 SettlementFailureCode::InvalidObservation,
465 detail.as_str(),
466 ),
467 Self::Transient(detail) => (
468 SettlementFailureClass::Retryable,
469 SettlementFailureCode::Backend,
470 detail.as_str(),
471 ),
472 Self::Permanent(detail) => (
473 SettlementFailureClass::Permanent,
474 SettlementFailureCode::Backend,
475 detail.as_str(),
476 ),
477 Self::Pipeline(error) => match error {
478 SettlementError::Rpc(detail) => (
479 SettlementFailureClass::Retryable,
480 SettlementFailureCode::Rpc,
481 detail.as_str(),
482 ),
483 SettlementError::InvalidInput(detail) => (
484 SettlementFailureClass::Permanent,
485 SettlementFailureCode::InvalidInput,
486 detail.as_str(),
487 ),
488 SettlementError::InvalidDispatch(detail) => (
489 SettlementFailureClass::Permanent,
490 SettlementFailureCode::InvalidDispatch,
491 detail.as_str(),
492 ),
493 SettlementError::InvalidBinding(detail) => (
494 SettlementFailureClass::Permanent,
495 SettlementFailureCode::InvalidBinding,
496 detail.as_str(),
497 ),
498 SettlementError::Unsupported(detail) => (
499 SettlementFailureClass::Permanent,
500 SettlementFailureCode::Unsupported,
501 detail.as_str(),
502 ),
503 SettlementError::Serialization(detail) => (
504 SettlementFailureClass::Permanent,
505 SettlementFailureCode::Serialization,
506 detail.as_str(),
507 ),
508 SettlementError::Signature(detail) => (
509 SettlementFailureClass::Permanent,
510 SettlementFailureCode::Signature,
511 detail.as_str(),
512 ),
513 SettlementError::Verification(detail) => (
514 SettlementFailureClass::Permanent,
515 SettlementFailureCode::Verification,
516 detail.as_str(),
517 ),
518 },
519 };
520
521 (class, SettlementFailureReason::from_detail(code, detail))
522 }
523}
524
525pub trait SettlementHook: Send + Sync {
544 fn observe(
548 &self,
549 observation: &SettlementObservation,
550 idempotency_key: &SettlementIdempotencyKey,
551 ) -> Result<SettlementOutcome, SettlementHookError>;
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557
558 fn require_ok<T, E>(result: Result<T, E>, context: &'static str) -> T
559 where
560 E: std::fmt::Debug,
561 {
562 result.unwrap_or_else(|error| panic!("{context}: {error:?}"))
563 }
564
565 fn sample_amount() -> MonetaryAmount {
566 MonetaryAmount {
567 currency: "USD".to_string(),
568 units: 100,
569 }
570 }
571
572 fn failure(code: SettlementFailureCode, detail: &str) -> SettlementFailureReason {
573 SettlementFailureReason::from_detail(code, detail)
574 }
575
576 fn serialize<T: Serialize>(value: &T) -> String {
577 match serde_json::to_string(value) {
578 Ok(encoded) => encoded,
579 Err(error) => panic!("value must serialize: {error}"),
580 }
581 }
582
583 #[test]
584 fn observation_schema_is_stable() {
585 assert_eq!(SETTLEMENT_OBSERVATION_SCHEMA, "chio.settle.observation.v1");
586 }
587
588 #[test]
589 fn outcome_schema_is_stable() {
590 assert_eq!(SETTLEMENT_OUTCOME_SCHEMA, "chio.settle.outcome.v1");
591 }
592
593 #[test]
594 fn outcome_deserialization_rejects_an_unsupported_schema() {
595 let result = serde_json::from_value::<SettlementOutcome>(serde_json::json!({
596 "kind": "accepted",
597 "schema": "chio.settle.outcome.v99",
598 "transcript_id": "transcript-1",
599 }));
600
601 assert!(result.is_err());
602 }
603
604 #[test]
605 fn ordering_key_sorts_by_finalized_at_then_receipt_id() {
606 let a = SettlementObservation::new(
607 "rcpt-b",
608 10,
609 "srv",
610 "tool",
611 "cap",
612 sample_amount(),
613 "ch",
614 "ph",
615 );
616 let b = SettlementObservation::new(
617 "rcpt-a",
618 10,
619 "srv",
620 "tool",
621 "cap",
622 sample_amount(),
623 "ch",
624 "ph",
625 );
626 let c = SettlementObservation::new(
627 "rcpt-c",
628 5,
629 "srv",
630 "tool",
631 "cap",
632 sample_amount(),
633 "ch",
634 "ph",
635 );
636 let mut frames = [a.clone(), b.clone(), c.clone()];
637 frames.sort_by(|left, right| left.ordering_key().cmp(&right.ordering_key()));
638 assert_eq!(frames[0].receipt_id, "rcpt-c");
639 assert_eq!(frames[1].receipt_id, "rcpt-a");
640 assert_eq!(frames[2].receipt_id, "rcpt-b");
641 }
642
643 #[test]
644 fn outcome_classifiers_match_constructors() {
645 let retry = SettlementOutcome::retryable(failure(SettlementFailureCode::Rpc, "rpc lag"));
646 assert!(retry.is_retryable());
647 assert!(!retry.is_permanent());
648
649 let dead = SettlementOutcome::permanent(failure(
650 SettlementFailureCode::InvalidObservation,
651 "policy denied",
652 ));
653 assert!(!dead.is_retryable());
654 assert!(dead.is_permanent());
655
656 let skip = SettlementOutcome::skipped(SettlementSkipReason::ZeroCharge);
657 assert!(!skip.is_retryable());
658 assert!(!skip.is_permanent());
659
660 let ok = SettlementOutcome::accepted("ts-1");
661 assert!(!ok.is_retryable());
662 assert!(!ok.is_permanent());
663 }
664
665 #[test]
666 fn retryable_constructor_rejects_a_known_permanent_code() {
667 let outcome = SettlementOutcome::retryable(failure(
668 SettlementFailureCode::InvalidReceiptSignature,
669 "invalid signature",
670 ));
671
672 assert!(matches!(outcome, SettlementOutcome::Permanent { .. }));
673 }
674
675 #[test]
676 fn outcome_predicates_reject_a_forged_retryable_shape() {
677 let outcome = match serde_json::from_value::<SettlementOutcome>(serde_json::json!({
678 "kind": "retryable",
679 "schema": SETTLEMENT_OUTCOME_SCHEMA,
680 "reason": {
681 "code": "invalid_receipt_signature",
682 "detail_sha256": vec![0_u8; 32],
683 },
684 })) {
685 Ok(outcome) => outcome,
686 Err(error) => panic!("test outcome deserialization failed: {error}"),
687 };
688
689 assert!(!outcome.is_retryable());
690 assert!(outcome.is_permanent());
691 }
692
693 #[test]
696 fn settlement_hook_is_object_safe() {
697 struct NoopHook;
698 impl SettlementHook for NoopHook {
699 fn observe(
700 &self,
701 observation: &SettlementObservation,
702 _idempotency_key: &SettlementIdempotencyKey,
703 ) -> Result<SettlementOutcome, SettlementHookError> {
704 if observation.amount.units == 0 {
705 return Ok(SettlementOutcome::skipped(SettlementSkipReason::ZeroCharge));
706 }
707 Ok(SettlementOutcome::accepted(format!(
708 "ts-{}",
709 observation.receipt_id
710 )))
711 }
712 }
713 let hook: std::sync::Arc<dyn SettlementHook> = std::sync::Arc::new(NoopHook);
714 let observation = SettlementObservation::new(
715 "rcpt-1",
716 42,
717 "srv",
718 "tool",
719 "cap",
720 sample_amount(),
721 "ch",
722 "ph",
723 );
724 let outcome = require_ok(
725 hook.observe(
726 &observation,
727 &SettlementIdempotencyKey {
728 receipt_id: observation.receipt_id.clone(),
729 row_version: 1,
730 },
731 ),
732 "hook returns observed outcome",
733 );
734 assert!(matches!(outcome, SettlementOutcome::Accepted { .. }));
735 }
736
737 #[test]
738 fn hook_errors_have_typed_bounded_classification() {
739 let cases = [
740 (
741 SettlementHookError::InvalidObservation("secret".to_string()),
742 SettlementFailureClass::Permanent,
743 SettlementFailureCode::InvalidObservation,
744 ),
745 (
746 SettlementHookError::Transient("secret".to_string()),
747 SettlementFailureClass::Retryable,
748 SettlementFailureCode::Backend,
749 ),
750 (
751 SettlementHookError::Permanent("secret".to_string()),
752 SettlementFailureClass::Permanent,
753 SettlementFailureCode::Backend,
754 ),
755 (
756 SettlementHookError::Pipeline(SettlementError::Rpc("secret".to_string())),
757 SettlementFailureClass::Retryable,
758 SettlementFailureCode::Rpc,
759 ),
760 (
761 SettlementHookError::Pipeline(SettlementError::InvalidInput("secret".to_string())),
762 SettlementFailureClass::Permanent,
763 SettlementFailureCode::InvalidInput,
764 ),
765 (
766 SettlementHookError::Pipeline(SettlementError::InvalidDispatch(
767 "secret".to_string(),
768 )),
769 SettlementFailureClass::Permanent,
770 SettlementFailureCode::InvalidDispatch,
771 ),
772 (
773 SettlementHookError::Pipeline(SettlementError::InvalidBinding(
774 "secret".to_string(),
775 )),
776 SettlementFailureClass::Permanent,
777 SettlementFailureCode::InvalidBinding,
778 ),
779 (
780 SettlementHookError::Pipeline(SettlementError::Unsupported("secret".to_string())),
781 SettlementFailureClass::Permanent,
782 SettlementFailureCode::Unsupported,
783 ),
784 (
785 SettlementHookError::Pipeline(SettlementError::Serialization("secret".to_string())),
786 SettlementFailureClass::Permanent,
787 SettlementFailureCode::Serialization,
788 ),
789 (
790 SettlementHookError::Pipeline(SettlementError::Signature("secret".to_string())),
791 SettlementFailureClass::Permanent,
792 SettlementFailureCode::Signature,
793 ),
794 (
795 SettlementHookError::Pipeline(SettlementError::Verification("secret".to_string())),
796 SettlementFailureClass::Permanent,
797 SettlementFailureCode::Verification,
798 ),
799 ];
800
801 for (error, expected_class, expected_code) in cases {
802 let (class, reason) = error.classification();
803 assert_eq!(class, expected_class);
804 assert_eq!(reason.code(), expected_code);
805 assert_eq!(
806 reason.detail_sha256(),
807 chio_core::hashing::sha256(b"secret").as_bytes()
808 );
809 let encoded = serialize(&reason);
810 assert!(!encoded.contains("secret"));
811 }
812 }
813
814 #[test]
815 fn failure_reason_digest_is_deterministic_and_private() {
816 let reason = failure(SettlementFailureCode::Rpc, "sensitive detail");
817 let restored = SettlementFailureReason::from_digest(reason.code(), *reason.detail_sha256());
818
819 assert_eq!(reason, restored);
820 assert_eq!(
821 reason.detail_sha256(),
822 chio_core::hashing::sha256(b"sensitive detail").as_bytes()
823 );
824 assert!(!serialize(&reason).contains("sensitive detail"));
825 }
826
827 #[test]
828 fn failure_code_labels_match_the_serialized_contract() {
829 let cases = [
830 SettlementFailureCode::InvalidReceiptSignature,
831 SettlementFailureCode::InvalidActionHash,
832 SettlementFailureCode::UntrustedReceiptSigner,
833 SettlementFailureCode::MalformedFinancialMetadata,
834 SettlementFailureCode::InvalidObservation,
835 SettlementFailureCode::Rpc,
836 SettlementFailureCode::InvalidInput,
837 SettlementFailureCode::InvalidDispatch,
838 SettlementFailureCode::InvalidBinding,
839 SettlementFailureCode::Unsupported,
840 SettlementFailureCode::Serialization,
841 SettlementFailureCode::Signature,
842 SettlementFailureCode::Verification,
843 SettlementFailureCode::Backend,
844 ];
845
846 for code in cases {
847 let serialized = match serde_json::to_value(code) {
848 Ok(serialized) => serialized,
849 Err(error) => panic!("failure code serialization failed: {error}"),
850 };
851 assert_eq!(
852 serialized,
853 serde_json::Value::String(code.as_str().to_string())
854 );
855 assert_eq!(SettlementFailureCode::try_from(code.as_str()), Ok(code));
856 }
857 }
858
859 #[test]
860 fn failure_code_parser_rejects_unknown_labels() {
861 for label in ["", "RPC", "rpc ", "unknown"] {
862 assert_eq!(
863 SettlementFailureCode::try_from(label),
864 Err(SettlementFailureCodeParseError)
865 );
866 }
867 assert_eq!(
868 SettlementFailureCodeParseError.to_string(),
869 "unknown settlement failure code"
870 );
871 }
872}