Selaa lähdekoodia

Relocate the insufficient-funds error beside the selection code

posting_selection.rs was a shallow, mis-named module: a file whose only
contents were a one-variant error enum and its trait impls, with no
selection logic at all. The actual posting selection (greedy
largest-first) lives in posting_resolution.rs, so the module name
misdirected a reader looking for where selection happens.

The error is also not resolve-internal plumbing. InsufficientFunds is a
shared domain leaf with two independent producers: resolve_envelope
raises it during selection, and the inflight over-confirm guard
constructs it directly without any resolution. LedgerError flattens it
into a public Selection variant. Folding it into ResolveError would bury
a shared error under a type one of its producers never creates.

Collapse the one-variant enum to a struct (selection has exactly one
failure mode) and move it next to the code that raises it, in
posting_resolution.rs. Drop the two From impls that were never
exercised. The Selection variant names and the public LedgerError
surface are unchanged apart from the payload type.
Cesar Rodas 3 päivää sitten
vanhempi
säilyke
111880f294

+ 2 - 3
crates/kuatia-core/src/lib.rs

@@ -6,7 +6,6 @@
 
 pub mod hash;
 pub mod posting_resolution;
-pub mod posting_selection;
 pub mod validate;
 
 pub use kuatia_types::*;
@@ -16,7 +15,7 @@ pub use hash::{
     double_sha256, envelope_id,
 };
 pub use posting_resolution::{
-    Debit, MovementDraft, ResolveError, ResolveInput, draft_movements, resolve_envelope,
+    Debit, InsufficientFunds, MovementDraft, ResolveError, ResolveInput, draft_movements,
+    resolve_envelope,
 };
-pub use posting_selection::SelectionError;
 pub use validate::{Plan, PlanInput, ValidationError, validate_and_plan};

+ 33 - 14
crates/kuatia-core/src/posting_resolution.rs

@@ -15,7 +15,6 @@
 
 use std::collections::{HashMap, HashSet};
 
-use crate::posting_selection::SelectionError;
 use kuatia_types::{
     AccountId, AssetId, Cent, Envelope, EnvelopeBuilder, NewPosting, OverflowError, Posting,
     PostingId, Transfer,
@@ -25,12 +24,38 @@ use kuatia_types::{
 // Errors
 // ---------------------------------------------------------------------------
 
+/// Available postings do not cover a requested amount.
+///
+/// When a caller uses `pay` or `withdraw` they specify an amount, not which
+/// postings to consume. Selection picks the postings; when a non-overdraft
+/// account cannot cover the amount it fails with this. Selection has this one
+/// failure mode, so it is a struct, not an enum; overflow is reported separately
+/// by [`ResolveError`].
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct InsufficientFunds {
+    /// Total value of eligible postings.
+    pub available: Cent,
+    /// Amount the caller asked for.
+    pub requested: Cent,
+}
+
+impl std::fmt::Display for InsufficientFunds {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "insufficient funds: available {}, requested {}",
+            self.available, self.requested
+        )
+    }
+}
+
+impl std::error::Error for InsufficientFunds {}
+
 /// Failure from resolution pass 2 ([`resolve_envelope`]).
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum ResolveError {
-    /// Posting selection failed: insufficient funds for a non-overdraft account,
-    /// or an overflow while summing candidate postings.
-    Selection(SelectionError),
+    /// Posting selection failed: insufficient funds for a non-overdraft account.
+    Selection(InsufficientFunds),
     /// Monetary arithmetic overflowed while computing change or a shortfall.
     Overflow,
 }
@@ -53,12 +78,6 @@ impl std::error::Error for ResolveError {
     }
 }
 
-impl From<SelectionError> for ResolveError {
-    fn from(e: SelectionError) -> Self {
-        Self::Selection(e)
-    }
-}
-
 impl From<OverflowError> for ResolveError {
     fn from(_: OverflowError) -> Self {
         Self::Overflow
@@ -144,7 +163,7 @@ pub struct ResolveInput<'a> {
     /// Accounts that permit overdraft (i.e. that do *not* carry
     /// `DEBIT_MUST_NOT_EXCEED_CREDIT`). A debit short of positive postings gets a
     /// negative offset posting only if its account is in this set; otherwise it
-    /// fails with [`SelectionError::InsufficientFunds`]. A missing account is
+    /// fails with [`InsufficientFunds`]. A missing account is
     /// treated as forbidding overdraft, so an unknown account never gets an
     /// offset position on unknown terms.
     pub overdraft_allowed: &'a HashSet<AccountId>,
@@ -225,7 +244,7 @@ pub fn resolve_envelope(input: ResolveInput<'_>) -> Result<Envelope, ResolveErro
                     payer: None,
                 });
             } else {
-                return Err(ResolveError::Selection(SelectionError::InsufficientFunds {
+                return Err(ResolveError::Selection(InsufficientFunds {
                     available: total_positive,
                     requested: debit.amount,
                 }));
@@ -397,7 +416,7 @@ mod tests {
         .unwrap_err();
         assert_eq!(
             err,
-            ResolveError::Selection(SelectionError::InsufficientFunds {
+            ResolveError::Selection(InsufficientFunds {
                 available: Cent::from(40),
                 requested: Cent::from(100),
             })
@@ -419,7 +438,7 @@ mod tests {
         .unwrap_err();
         assert_eq!(
             err,
-            ResolveError::Selection(SelectionError::InsufficientFunds {
+            ResolveError::Selection(InsufficientFunds {
                 available: Cent::ZERO,
                 requested: Cent::from(100),
             })

+ 0 - 38
crates/kuatia-core/src/posting_selection.rs

@@ -1,38 +0,0 @@
-//! The error raised when an intent cannot be covered by the available postings.
-//!
-//! When a caller uses `pay` or `withdraw`, they specify an amount — not which
-//! postings to consume. Resolution ([`crate::posting_resolution::resolve_envelope`])
-//! picks the postings; when a non-overdraft account cannot cover the requested
-//! amount it fails with [`SelectionError::InsufficientFunds`].
-
-use kuatia_types::Cent;
-
-/// Error returned when an amount cannot be covered by the available postings.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum SelectionError {
-    /// Available postings do not cover the requested amount.
-    InsufficientFunds {
-        /// Total value of eligible postings.
-        available: Cent,
-        /// Amount the caller asked for.
-        requested: Cent,
-    },
-}
-
-impl std::fmt::Display for SelectionError {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Self::InsufficientFunds {
-                available,
-                requested,
-            } => {
-                write!(
-                    f,
-                    "insufficient funds: available {available}, requested {requested}"
-                )
-            }
-        }
-    }
-}
-
-impl std::error::Error for SelectionError {}

+ 3 - 9
crates/kuatia/src/error.rs

@@ -4,8 +4,8 @@
 //! and from storage, so callers get a single error type from every API.
 
 use kuatia_core::{
-    AccountId, AssetId, BookId, EnvelopeId, OverflowError, PostingId, ResolveError, SelectionError,
-    ValidationError,
+    AccountId, AssetId, BookId, EnvelopeId, InsufficientFunds, OverflowError, PostingId,
+    ResolveError, ValidationError,
 };
 use kuatia_storage::error::StoreError;
 
@@ -24,7 +24,7 @@ pub enum LedgerError {
     /// Storage operation failed.
     Store(StoreError),
     /// Posting selection failed (e.g. insufficient funds).
-    Selection(SelectionError),
+    Selection(InsufficientFunds),
     /// The referenced transfer does not exist.
     TransferNotFound(EnvelopeId),
     /// The posting cannot be reversed (e.g. already consumed).
@@ -128,12 +128,6 @@ impl From<StoreError> for LedgerError {
     }
 }
 
-impl From<SelectionError> for LedgerError {
-    fn from(e: SelectionError) -> Self {
-        LedgerError::Selection(e)
-    }
-}
-
 impl From<ResolveError> for LedgerError {
     fn from(e: ResolveError) -> Self {
         match e {

+ 3 - 3
crates/kuatia/src/inflight.rs

@@ -16,8 +16,8 @@ use std::collections::{BTreeMap, BTreeSet};
 use std::sync::Arc;
 
 use kuatia_core::{
-    Account, AccountFlags, AccountId, AssetId, BookId, Cent, EnvelopeId, Metadata, Receipt,
-    SelectionError, Transfer, TransferBuilder, hash::double_sha256,
+    Account, AccountFlags, AccountId, AssetId, BookId, Cent, EnvelopeId, InsufficientFunds,
+    Metadata, Receipt, Transfer, TransferBuilder, hash::double_sha256,
 };
 use kuatia_storage::error::StoreError;
 use kuatia_storage::store::EnvelopeRecord;
@@ -318,7 +318,7 @@ impl Ledger {
                 })?;
             let held = self.balance(&leg.hold, &m.asset).await?;
             if m.amount > held {
-                return Err(LedgerError::Selection(SelectionError::InsufficientFunds {
+                return Err(LedgerError::Selection(InsufficientFunds {
                     available: held,
                     requested: m.amount,
                 }));

+ 1 - 4
crates/kuatia/tests/saga.rs

@@ -140,10 +140,7 @@ async fn saga_compensation_on_failure() {
             // the overspend surfaces as `Selection(InsufficientFunds)` rather
             // than a stringified `Store(Internal)`.
             assert!(
-                matches!(
-                    err,
-                    LedgerError::Selection(SelectionError::InsufficientFunds { .. })
-                ),
+                matches!(err, LedgerError::Selection(InsufficientFunds { .. })),
                 "expected typed InsufficientFunds, got {err:?}"
             );
             // The deposit should have been compensated (reversed)