error.rs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. //! Error types for the async ledger layer.
  2. //!
  3. //! [`LedgerError`] unifies errors from the pure core (validation, selection)
  4. //! and from storage, so callers get a single error type from every API.
  5. use kuatia_core::{
  6. AccountId, AssetId, BookId, EnvelopeId, InsufficientFunds, OverflowError, PostingId,
  7. ResolveError, ValidationError,
  8. };
  9. use kuatia_storage::error::StoreError;
  10. /// Unified error type for the async ledger API.
  11. ///
  12. /// `Clone` so the saga engine can carry a typed error across its step seam and
  13. /// return the real variant to the caller (an [`OverdraftExceeded`] detected
  14. /// during commit stays an [`OverdraftExceeded`], not a stringified internal
  15. /// fault).
  16. ///
  17. /// [`OverdraftExceeded`]: ValidationError::OverdraftExceeded
  18. #[derive(Debug, Clone)]
  19. pub enum LedgerError {
  20. /// A transfer invariant was violated.
  21. Validation(ValidationError),
  22. /// Storage operation failed.
  23. Store(StoreError),
  24. /// Posting selection failed (e.g. insufficient funds).
  25. Selection(InsufficientFunds),
  26. /// The referenced transfer does not exist.
  27. TransferNotFound(EnvelopeId),
  28. /// The posting cannot be reversed (e.g. already consumed).
  29. PostingNotReversible(PostingId),
  30. /// The referenced account does not exist.
  31. AccountNotFound(AccountId),
  32. /// Cannot close an account that still has active postings.
  33. AccountNotEmpty(AccountId),
  34. /// The account is already closed.
  35. AccountAlreadyClosed(AccountId),
  36. /// A transfer named a book that does not exist.
  37. BookNotFound(BookId),
  38. /// The referenced inflight transaction does not exist (no authorize record).
  39. InflightNotFound(EnvelopeId),
  40. /// The referenced transfer is not an inflight authorize, or its metadata is
  41. /// malformed.
  42. NotInflightTransaction(EnvelopeId),
  43. /// The destination already has an open inflight hold; only one is allowed at
  44. /// a time per account.
  45. InflightAlreadyOpen(AccountId),
  46. /// The inflight transaction has no leg matching this destination and asset.
  47. InflightLegNotFound {
  48. /// The destination account with no matching leg.
  49. destination: AccountId,
  50. /// The asset with no matching leg.
  51. asset: AssetId,
  52. },
  53. /// An inflight movement must move between two distinct accounts.
  54. InflightSelfMovement(AccountId),
  55. /// Monetary arithmetic overflow.
  56. Overflow,
  57. /// A saga step failed and its compensation also failed.
  58. CompensationFailed {
  59. /// The error that triggered compensation.
  60. original: Box<LedgerError>,
  61. /// The error that occurred during compensation.
  62. compensation: Box<LedgerError>,
  63. },
  64. }
  65. impl std::fmt::Display for LedgerError {
  66. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  67. match self {
  68. Self::Validation(e) => write!(f, "validation: {e}"),
  69. Self::Store(e) => write!(f, "store: {e}"),
  70. Self::Selection(e) => write!(f, "selection: {e}"),
  71. Self::TransferNotFound(id) => write!(f, "transfer not found: {id:?}"),
  72. Self::PostingNotReversible(id) => write!(f, "posting not reversible: {id:?}"),
  73. Self::AccountNotFound(id) => write!(f, "account not found: {id:?}"),
  74. Self::AccountNotEmpty(id) => write!(f, "account not empty: {id:?}"),
  75. Self::AccountAlreadyClosed(id) => write!(f, "account already closed: {id:?}"),
  76. Self::BookNotFound(id) => write!(f, "book not found: {id:?}"),
  77. Self::InflightNotFound(id) => write!(f, "inflight transaction not found: {id:?}"),
  78. Self::NotInflightTransaction(id) => {
  79. write!(f, "not an inflight authorize transaction: {id:?}")
  80. }
  81. Self::InflightAlreadyOpen(id) => {
  82. write!(f, "account already has an open inflight hold: {id:?}")
  83. }
  84. Self::InflightLegNotFound { destination, asset } => write!(
  85. f,
  86. "inflight leg not found for destination {destination:?} asset {asset:?}"
  87. ),
  88. Self::InflightSelfMovement(id) => {
  89. write!(f, "inflight movement must have distinct from/to: {id:?}")
  90. }
  91. Self::Overflow => write!(f, "monetary amount overflow"),
  92. Self::CompensationFailed {
  93. original,
  94. compensation,
  95. } => write!(
  96. f,
  97. "compensation failed: original={original}, compensation={compensation}"
  98. ),
  99. }
  100. }
  101. }
  102. impl std::error::Error for LedgerError {
  103. fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
  104. match self {
  105. Self::Validation(e) => Some(e),
  106. Self::Store(e) => Some(e),
  107. Self::Selection(e) => Some(e),
  108. Self::Overflow => Some(&OverflowError),
  109. Self::CompensationFailed { original, .. } => Some(original.as_ref()),
  110. _ => None,
  111. }
  112. }
  113. }
  114. impl From<ValidationError> for LedgerError {
  115. fn from(e: ValidationError) -> Self {
  116. LedgerError::Validation(e)
  117. }
  118. }
  119. impl From<StoreError> for LedgerError {
  120. fn from(e: StoreError) -> Self {
  121. LedgerError::Store(e)
  122. }
  123. }
  124. impl From<ResolveError> for LedgerError {
  125. fn from(e: ResolveError) -> Self {
  126. match e {
  127. ResolveError::Selection(s) => LedgerError::Selection(s),
  128. ResolveError::Overflow => LedgerError::Overflow,
  129. }
  130. }
  131. }
  132. impl From<OverflowError> for LedgerError {
  133. fn from(_: OverflowError) -> Self {
  134. LedgerError::Overflow
  135. }
  136. }