Browse Source

Move mint quote mutation logic from database layer to domain model

Consolidates `increment_mint_quote_amount_paid` and
`increment_mint_quote_amount_issued` database methods into a single
`update_mint_quote` method, while relocating the business logic to the
`MintQuote` domain model itself.

The previous design had the database layer responsible for validating duplicate
payment IDs, checking over-issuance constraints, and incrementing counters.
This conflated persistence concerns with business rules, making the database
trait unnecessarily complex and coupling validation logic to database
operations.

The new approach introduces a change-tracking pattern where `MintQuote` exposes
`add_payment()` and `add_issuance()` methods that perform all validation and
record changes internally via a `MintQuoteChange` struct. The database layer
simply persists whatever changes the domain model has accumulated.

This separation provides several benefits:
- Validation errors are now proper domain errors (`DuplicatePaymentId`) rather
  than database errors
- Business rules can be tested without database involvement
- The database trait becomes a pure persistence concern
- Callers get immediate feedback from domain methods before any database
  interaction
Cesar Rodas 1 tháng trước cách đây
mục cha
commit
b54ffa1ab3

+ 20 - 11
crates/cdk-common/src/database/mint/mod.rs

@@ -124,19 +124,28 @@ pub trait QuotesTransaction {
         quote: MintMintQuote,
     ) -> Result<Acquired<MintMintQuote>, Self::Err>;
 
-    /// Increment amount paid [`MintMintQuote`]
-    async fn increment_mint_quote_amount_paid(
-        &mut self,
-        quote: &mut Acquired<mint::MintQuote>,
-        amount_paid: Amount,
-        payment_id: String,
-    ) -> Result<(), Self::Err>;
-
-    /// Increment amount paid [`MintMintQuote`]
-    async fn increment_mint_quote_amount_issued(
+    /// Persists any pending changes made to the mint quote.
+    ///
+    /// This method extracts changes accumulated in the quote (via [`mint::MintQuote::take_changes`])
+    /// and persists them to the database. Changes may include new payments received or new
+    /// issuances recorded against the quote.
+    ///
+    /// If no changes are pending, this method returns successfully without performing
+    /// any database operations.
+    ///
+    /// # Arguments
+    ///
+    /// * `quote` - A mutable reference to an acquired (row-locked) mint quote. The quote
+    ///   must be locked to ensure transactional consistency when persisting changes.
+    ///
+    /// # Implementation Notes
+    ///
+    /// Implementations should call [`mint::MintQuote::take_changes`] to retrieve pending
+    /// changes, then persist each payment and issuance record, and finally update the
+    /// quote's aggregate counters (`amount_paid`, `amount_issued`) in the database.
+    async fn update_mint_quote(
         &mut self,
         quote: &mut Acquired<mint::MintQuote>,
-        amount_issued: Amount,
     ) -> Result<(), Self::Err>;
 
     /// Get [`mint::MeltQuote`] and lock it for update in this transaction

+ 67 - 69
crates/cdk-common/src/database/mint/test/mint.rs

@@ -94,15 +94,17 @@ where
     let p1 = unique_string();
     let p2 = unique_string();
 
-    tx.increment_mint_quote_amount_paid(&mut mint_quote, 100.into(), p1.clone())
-        .await
+    mint_quote
+        .add_payment(100.into(), p1.clone(), None)
         .unwrap();
+    tx.update_mint_quote(&mut mint_quote).await.unwrap();
 
     assert_eq!(mint_quote.amount_paid(), 100.into());
 
-    tx.increment_mint_quote_amount_paid(&mut mint_quote, 250.into(), p2.clone())
-        .await
+    mint_quote
+        .add_payment(250.into(), p2.clone(), None)
         .unwrap();
+    tx.update_mint_quote(&mut mint_quote).await.unwrap();
 
     assert_eq!(mint_quote.amount_paid(), 350.into());
 
@@ -150,15 +152,17 @@ where
 
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
-    tx.increment_mint_quote_amount_paid(&mut mint_quote, 100.into(), p1.clone())
-        .await
+    mint_quote
+        .add_payment(100.into(), p1.clone(), None)
         .unwrap();
+    tx.update_mint_quote(&mut mint_quote).await.unwrap();
 
     assert_eq!(mint_quote.amount_paid(), 100.into());
 
-    tx.increment_mint_quote_amount_paid(&mut mint_quote, 250.into(), p2.clone())
-        .await
+    mint_quote
+        .add_payment(250.into(), p2.clone(), None)
         .unwrap();
+    tx.update_mint_quote(&mut mint_quote).await.unwrap();
     assert_eq!(mint_quote.amount_paid(), 350.into());
     tx.commit().await.unwrap();
 
@@ -211,14 +215,13 @@ where
 
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
-    tx.increment_mint_quote_amount_paid(&mut mint_quote, 100.into(), p1.clone())
-        .await
+    mint_quote
+        .add_payment(100.into(), p1.clone(), None)
         .unwrap();
+    tx.update_mint_quote(&mut mint_quote).await.unwrap();
 
-    assert!(tx
-        .increment_mint_quote_amount_paid(&mut mint_quote, 100.into(), p1)
-        .await
-        .is_err());
+    // Duplicate payment should fail
+    assert!(mint_quote.add_payment(100.into(), p1, None).is_err());
     tx.commit().await.unwrap();
 
     let mint_quote_from_db = db
@@ -255,9 +258,10 @@ where
 
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
-    tx.increment_mint_quote_amount_paid(&mut mint_quote, 100.into(), p1.clone())
-        .await
+    mint_quote
+        .add_payment(100.into(), p1.clone(), None)
         .unwrap();
+    tx.update_mint_quote(&mut mint_quote).await.unwrap();
     tx.commit().await.unwrap();
 
     let mut tx = Database::begin_transaction(&db).await.unwrap();
@@ -266,10 +270,8 @@ where
         .await
         .expect("no error")
         .expect("quote");
-    assert!(tx
-        .increment_mint_quote_amount_paid(&mut mint_quote, 100.into(), p1)
-        .await
-        .is_err());
+    // Duplicate payment should fail
+    assert!(mint_quote.add_payment(100.into(), p1, None).is_err());
     tx.commit().await.unwrap(); // although in theory nothing has changed, let's try it out
 
     let mint_quote_from_db = db
@@ -304,10 +306,8 @@ where
 
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
-    assert!(tx
-        .increment_mint_quote_amount_issued(&mut mint_quote, 100.into())
-        .await
-        .is_err());
+    // Trying to issue without any payment should fail (over-issue)
+    assert!(mint_quote.add_issuance(100.into()).is_err());
 }
 
 /// Reject over issue
@@ -341,10 +341,8 @@ where
         .await
         .expect("no error")
         .expect("quote");
-    assert!(tx
-        .increment_mint_quote_amount_issued(&mut mint_quote, 100.into())
-        .await
-        .is_err());
+    // Trying to issue without any payment should fail (over-issue)
+    assert!(mint_quote.add_issuance(100.into()).is_err());
 }
 
 /// Reject over issue with payment
@@ -371,13 +369,12 @@ where
     let p1 = unique_string();
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
-    tx.increment_mint_quote_amount_paid(&mut mint_quote, 100.into(), p1.clone())
-        .await
+    mint_quote
+        .add_payment(100.into(), p1.clone(), None)
         .unwrap();
-    assert!(tx
-        .increment_mint_quote_amount_issued(&mut mint_quote, 101.into())
-        .await
-        .is_err());
+    tx.update_mint_quote(&mut mint_quote).await.unwrap();
+    // Trying to issue more than paid should fail (over-issue)
+    assert!(mint_quote.add_issuance(101.into()).is_err());
 }
 
 /// Reject over issue with payment
@@ -405,9 +402,10 @@ where
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote).await.unwrap();
     let quote_id = mint_quote.id.clone();
-    tx.increment_mint_quote_amount_paid(&mut mint_quote, 100.into(), p1.clone())
-        .await
+    mint_quote
+        .add_payment(100.into(), p1.clone(), None)
         .unwrap();
+    tx.update_mint_quote(&mut mint_quote).await.unwrap();
     tx.commit().await.unwrap();
 
     let mut tx = Database::begin_transaction(&db).await.unwrap();
@@ -416,10 +414,8 @@ where
         .await
         .expect("no error")
         .expect("quote");
-    assert!(tx
-        .increment_mint_quote_amount_issued(&mut mint_quote, 101.into())
-        .await
-        .is_err());
+    // Trying to issue more than paid should fail (over-issue)
+    assert!(mint_quote.add_issuance(101.into()).is_err());
 }
 /// Successful melt with unique blinded messages
 pub async fn add_melt_request_unique_blinded_messages<DB>(db: DB)
@@ -1051,29 +1047,31 @@ where
     let mint_quote = tx.add_mint_quote(mint_quote).await.unwrap();
     tx.commit().await.unwrap();
 
-    // Increment amount paid first time
+    // Add payment first time
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx
         .get_mint_quote(&mint_quote.id)
         .await
         .expect("valid quote")
         .expect("valid result");
-    tx.increment_mint_quote_amount_paid(&mut mint_quote, 300.into(), "payment_1".to_string())
-        .await
+    mint_quote
+        .add_payment(300.into(), "payment_1".to_string(), None)
         .unwrap();
+    tx.update_mint_quote(&mut mint_quote).await.unwrap();
     assert_eq!(mint_quote.amount_paid(), 300.into());
     tx.commit().await.unwrap();
 
-    // Increment amount paid second time
+    // Add payment second time
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx
         .get_mint_quote(&mint_quote.id)
         .await
         .expect("valid quote")
         .expect("valid result");
-    tx.increment_mint_quote_amount_paid(&mut mint_quote, 200.into(), "payment_2".to_string())
-        .await
+    mint_quote
+        .add_payment(200.into(), "payment_2".to_string(), None)
         .unwrap();
+    tx.update_mint_quote(&mut mint_quote).await.unwrap();
     assert_eq!(mint_quote.amount_paid(), 500.into());
     tx.commit().await.unwrap();
 
@@ -1110,41 +1108,40 @@ where
     tx.add_mint_quote(mint_quote.clone()).await.unwrap();
     tx.commit().await.unwrap();
 
-    // First increment amount_paid to allow issuing
+    // First add payment to allow issuing
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx
         .get_mint_quote(&mint_quote.id)
         .await
         .expect("valid quote")
         .expect("valid result");
-    tx.increment_mint_quote_amount_paid(&mut mint_quote, 1000.into(), "payment_1".to_string())
-        .await
+    mint_quote
+        .add_payment(1000.into(), "payment_1".to_string(), None)
         .unwrap();
+    tx.update_mint_quote(&mut mint_quote).await.unwrap();
     tx.commit().await.unwrap();
 
-    // Increment amount issued first time
+    // Add issuance first time
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx
         .get_mint_quote(&mint_quote.id)
         .await
         .expect("valid quote")
         .expect("valid result");
-    tx.increment_mint_quote_amount_issued(&mut mint_quote, 400.into())
-        .await
-        .unwrap();
+    mint_quote.add_issuance(400.into()).unwrap();
+    tx.update_mint_quote(&mut mint_quote).await.unwrap();
     assert_eq!(mint_quote.amount_issued(), 400.into());
     tx.commit().await.unwrap();
 
-    // Increment amount issued second time
+    // Add issuance second time
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx
         .get_mint_quote(&mint_quote.id)
         .await
         .expect("valid quote")
         .expect("valid result");
-    tx.increment_mint_quote_amount_issued(&mut mint_quote, 300.into())
-        .await
-        .unwrap();
+    mint_quote.add_issuance(300.into()).unwrap();
+    tx.update_mint_quote(&mut mint_quote).await.unwrap();
     assert_eq!(mint_quote.amount_issued(), 700.into());
     tx.commit().await.unwrap();
 
@@ -1375,13 +1372,14 @@ where
         .await
         .expect("valid quote")
         .expect("valid result");
-    tx.increment_mint_quote_amount_paid(&mut mint_quote, 300.into(), "payment_1".to_string())
-        .await
+    mint_quote
+        .add_payment(300.into(), "payment_1".to_string(), None)
         .unwrap();
+    tx.update_mint_quote(&mut mint_quote).await.unwrap();
     assert_eq!(mint_quote.amount_paid(), 300.into());
     tx.commit().await.unwrap();
 
-    // Try to add the same payment_id again - should fail with Duplicate error
+    // Try to add the same payment_id again - should fail with DuplicatePaymentId error
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx
         .get_mint_quote(&mint_quote.id)
@@ -1389,12 +1387,10 @@ where
         .expect("valid quote")
         .expect("valid result");
 
-    let result = tx
-        .increment_mint_quote_amount_paid(&mut mint_quote, 300.into(), "payment_1".to_string())
-        .await;
+    let result = mint_quote.add_payment(300.into(), "payment_1".to_string(), None);
 
     assert!(
-        matches!(result.unwrap_err(), Error::Duplicate),
+        matches!(result.unwrap_err(), crate::Error::DuplicatePaymentId),
         "Duplicate payment_id should be rejected"
     );
     tx.rollback().await.unwrap();
@@ -1411,9 +1407,10 @@ where
         .expect("valid quote")
         .expect("valid result");
 
-    tx.increment_mint_quote_amount_paid(&mut mint_quote, 200.into(), "payment_2".to_string())
-        .await
+    mint_quote
+        .add_payment(200.into(), "payment_2".to_string(), None)
         .unwrap();
+    tx.update_mint_quote(&mut mint_quote).await.unwrap();
 
     assert_eq!(mint_quote.amount_paid(), 500.into());
     tx.commit().await.unwrap();
@@ -1461,9 +1458,10 @@ where
         .expect("quote should exist");
 
     // Now modification should succeed
-    let result = tx
-        .increment_mint_quote_amount_paid(&mut loaded_quote, 100.into(), unique_string())
-        .await;
+    loaded_quote
+        .add_payment(100.into(), unique_string(), None)
+        .unwrap();
+    let result = tx.update_mint_quote(&mut loaded_quote).await;
 
     assert!(
         result.is_ok(),

+ 101 - 24
crates/cdk-common/src/mint.rs

@@ -358,6 +358,24 @@ impl Operation {
     }
 }
 
+/// Tracks pending changes made to a [`MintQuote`] that need to be persisted.
+///
+/// This struct implements a change-tracking pattern that separates domain logic from
+/// persistence concerns. When modifications are made to a `MintQuote` via methods like
+/// [`MintQuote::add_payment`] or [`MintQuote::add_issuance`], the changes are recorded
+/// here rather than being immediately persisted. The database layer can then call
+/// [`MintQuote::take_changes`] to retrieve and persist only the modifications.
+///
+/// This approach allows business rule validation to happen in the domain model while
+/// keeping the database layer focused purely on persistence.
+#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
+pub struct MintQuoteChange {
+    /// New payments added since the quote was loaded or last persisted.
+    pub payments: Option<Vec<IncomingPayment>>,
+    /// New issuance amounts recorded since the quote was loaded or last persisted.
+    pub issuances: Option<Vec<Amount>>,
+}
+
 /// Mint Quote Info
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct MintQuote {
@@ -393,6 +411,13 @@ pub struct MintQuote {
     /// Payment of payment(s) that filled quote
     #[serde(default)]
     pub issuance: Vec<Issuance>,
+    /// Accumulated changes since this quote was loaded or created.
+    ///
+    /// This field is not serialized and is used internally to track modifications
+    /// that need to be persisted. Use [`Self::take_changes`] to extract pending
+    /// changes for persistence.
+    #[serde(skip)]
+    changes: Option<MintQuoteChange>,
 }
 
 impl MintQuote {
@@ -429,38 +454,40 @@ impl MintQuote {
             payment_method,
             payments,
             issuance,
+            changes: None,
         }
     }
 
-    /// Increment the amount paid on the mint quote by a given amount
-    #[instrument(skip(self))]
-    pub fn increment_amount_paid(
-        &mut self,
-        additional_amount: Amount,
-    ) -> Result<Amount, crate::Error> {
-        self.amount_paid = self
-            .amount_paid
-            .checked_add(additional_amount)
-            .ok_or(crate::Error::AmountOverflow)?;
-        Ok(self.amount_paid)
-    }
-
     /// Amount paid
     #[instrument(skip(self))]
     pub fn amount_paid(&self) -> Amount {
         self.amount_paid
     }
 
-    /// Increment the amount issued on the mint quote by a given amount
+    /// Records tokens being issued against this mint quote.
+    ///
+    /// This method validates that the issuance doesn't exceed the amount paid, updates
+    /// the quote's internal state, and records the change for later persistence. The
+    /// `amount_issued` counter is incremented and the issuance is added to the change
+    /// tracker for the database layer to persist.
+    ///
+    /// # Arguments
+    ///
+    /// * `additional_amount` - The amount of tokens being issued.
+    ///
+    /// # Returns
+    ///
+    /// Returns the new total `amount_issued` after this issuance is recorded.
     ///
     /// # Errors
-    /// Returns an error if the new issued amount would exceed the paid amount
-    /// (can't issue more than what's been paid) or if the addition would overflow.
+    ///
+    /// Returns [`crate::Error::OverIssue`] if the new issued amount would exceed the
+    /// amount paid (cannot issue more tokens than have been paid for).
+    ///
+    /// Returns [`crate::Error::AmountOverflow`] if adding the issuance amount would
+    /// cause an arithmetic overflow.
     #[instrument(skip(self))]
-    pub fn increment_amount_issued(
-        &mut self,
-        additional_amount: Amount,
-    ) -> Result<Amount, crate::Error> {
+    pub fn add_issuance(&mut self, additional_amount: Amount) -> Result<Amount, crate::Error> {
         let new_amount_issued = self
             .amount_issued
             .checked_add(additional_amount)
@@ -471,7 +498,14 @@ impl MintQuote {
             return Err(crate::Error::OverIssue);
         }
 
+        self.changes
+            .get_or_insert_default()
+            .issuances
+            .get_or_insert_default()
+            .push(additional_amount);
+
         self.amount_issued = new_amount_issued;
+
         Ok(self.amount_issued)
     }
 
@@ -502,16 +536,47 @@ impl MintQuote {
         self.amount_paid - self.amount_issued
     }
 
-    /// Add a payment ID to the list of payment IDs
+    /// Extracts and returns all pending changes, leaving the internal change tracker empty.
+    ///
+    /// This method is typically called by the database layer after loading or modifying a quote. It
+    /// returns any accumulated changes (new payments, issuances) that need to be persisted, and
+    /// clears the internal change buffer so that subsequent calls return `None` until new
+    /// modifications are made.
+    ///
+    /// Returns `None` if no changes have been made since the last call to this method or since the
+    /// quote was created/loaded.
+    pub fn take_changes(&mut self) -> Option<MintQuoteChange> {
+        self.changes.take()
+    }
+
+    /// Records a new payment received for this mint quote.
+    ///
+    /// This method validates the payment, updates the quote's internal state, and records the
+    /// change for later persistence. The `amount_paid` counter is incremented and the payment is
+    /// added to the change tracker for the database layer to persist.
+    ///
+    /// # Arguments
     ///
-    /// Returns an error if the payment ID is already in the list
+    /// * `amount` - The amount of the payment in the quote's currency unit. * `payment_id` - A
+    /// unique identifier for this payment (e.g., lightning payment hash). * `time` - Optional Unix
+    /// timestamp of when the payment was received. If `None`, the current time is used.
+    ///
+    /// # Errors
+    ///
+    /// Returns [`crate::Error::DuplicatePaymentId`] if a payment with the same ID has already been
+    /// recorded for this quote.
+    ///
+    /// Returns [`crate::Error::AmountOverflow`] if adding the payment amount would cause an
+    /// arithmetic overflow.
     #[instrument(skip(self))]
     pub fn add_payment(
         &mut self,
         amount: Amount,
         payment_id: String,
-        time: u64,
+        time: Option<u64>,
     ) -> Result<(), crate::Error> {
+        let time = time.unwrap_or_else(unix_time);
+
         let payment_ids = self.payment_ids();
         if payment_ids.contains(&&payment_id) {
             return Err(crate::Error::DuplicatePaymentId);
@@ -519,7 +584,19 @@ impl MintQuote {
 
         let payment = IncomingPayment::new(amount, payment_id, time);
 
-        self.payments.push(payment);
+        self.payments.push(payment.clone());
+
+        self.changes
+            .get_or_insert_default()
+            .payments
+            .get_or_insert_default()
+            .push(payment);
+
+        self.amount_paid = self
+            .amount_paid
+            .checked_add(amount)
+            .ok_or(crate::Error::AmountOverflow)?;
+
         Ok(())
     }
 

+ 10 - 7
crates/cdk-integration-tests/tests/mint.rs

@@ -208,13 +208,16 @@ async fn test_concurrent_duplicate_payment_handling() {
                 .await
                 .expect("no error")
                 .expect("some value");
-            let result = tx
-                .increment_mint_quote_amount_paid(
-                    &mut quote_from_db,
-                    Amount::from(10),
-                    payment_id_clone,
-                )
-                .await;
+
+            let result = if let Err(err) =
+                quote_from_db.add_payment(Amount::from(10), payment_id_clone, None)
+            {
+                Err(err)
+            } else {
+                tx.update_mint_quote(&mut quote_from_db)
+                    .await
+                    .map_err(|err| cdk_common::Error::Database(err))
+            };
 
             if result.is_ok() {
                 tx.commit().await.unwrap();

+ 51 - 101
crates/cdk-sql-common/src/mint/mod.rs

@@ -1012,127 +1012,77 @@ where
         Ok(())
     }
 
-    #[instrument(skip(self))]
-    async fn increment_mint_quote_amount_paid(
+    async fn update_mint_quote(
         &mut self,
         quote: &mut Acquired<mint::MintQuote>,
-        amount_paid: Amount,
-        payment_id: String,
     ) -> Result<(), Self::Err> {
-        if amount_paid == Amount::ZERO {
-            tracing::warn!("Amount payments of zero amount should not be recorded.");
-            return Err(Error::Duplicate);
-        }
+        let mut changes = if let Some(changes) = quote.take_changes() {
+            changes
+        } else {
+            return Ok(());
+        };
 
-        // Check if payment_id already exists in mint_quote_payments
-        let exists = query(
-            r#"
-            SELECT payment_id
-            FROM mint_quote_payments
-            WHERE payment_id = :payment_id
-            FOR UPDATE
-            "#,
-        )?
-        .bind("payment_id", payment_id.clone())
-        .fetch_one(&self.inner)
-        .await?;
+        if changes.issuances.is_none() && changes.payments.is_none() {
+            return Ok(());
+        }
 
-        if exists.is_some() {
-            tracing::error!("Payment ID already exists: {}", payment_id);
-            return Err(database::Error::Duplicate);
+        for payment in changes.payments.take().unwrap_or_default() {
+            query(
+                r#"
+                INSERT INTO mint_quote_payments
+                (quote_id, payment_id, amount, timestamp)
+                VALUES (:quote_id, :payment_id, :amount, :timestamp)
+                "#,
+            )?
+            .bind("quote_id", quote.id.to_string())
+            .bind("payment_id", payment.payment_id)
+            .bind("amount", payment.amount.to_i64())
+            .bind("timestamp", payment.time as i64)
+            .execute(&self.inner)
+            .await
+            .map_err(|err| {
+                tracing::error!("SQLite could not insert payment ID: {}", err);
+                err
+            })?;
         }
 
-        let current_amount_paid = quote.amount_paid();
-        let new_amount_paid = quote
-            .increment_amount_paid(amount_paid)
-            .map_err(|_| Error::AmountOverflow)?;
+        let current_time = unix_time();
 
-        tracing::debug!(
-            "Mint quote {} amount paid was {} is now {}.",
-            quote.id,
-            current_amount_paid,
-            new_amount_paid
-        );
+        for amount_issued in changes.issuances.take().unwrap_or_default() {
+            query(
+                r#"
+                INSERT INTO mint_quote_issued
+                (quote_id, amount, timestamp)
+                VALUES (:quote_id, :amount, :timestamp);
+                "#,
+            )?
+            .bind("quote_id", quote.id.to_string())
+            .bind("amount", amount_issued.to_i64())
+            .bind("timestamp", current_time as i64)
+            .execute(&self.inner)
+            .await?;
+        }
 
-        // Update the amount_paid
         query(
             r#"
-            UPDATE mint_quote
-            SET amount_paid = :amount_paid
-            WHERE id = :quote_id
+            UPDATE
+                mint_quote
+            SET
+                amount_issued = :amount_issued,
+                amount_paid = :amount_paid
+            WHERE
+                id = :quote_id
             "#,
         )?
-        .bind("amount_paid", new_amount_paid.to_i64())
         .bind("quote_id", quote.id.to_string())
+        .bind("amount_issued", quote.amount_issued().to_i64())
+        .bind("amount_paid", quote.amount_paid().to_i64())
         .execute(&self.inner)
         .await
         .inspect_err(|err| {
             tracing::error!("SQLite could not update mint quote amount_paid: {}", err);
         })?;
 
-        // Add payment_id to mint_quote_payments table
-        query(
-            r#"
-            INSERT INTO mint_quote_payments
-            (quote_id, payment_id, amount, timestamp)
-            VALUES (:quote_id, :payment_id, :amount, :timestamp)
-            "#,
-        )?
-        .bind("quote_id", quote.id.to_string())
-        .bind("payment_id", payment_id)
-        .bind("amount", amount_paid.to_i64())
-        .bind("timestamp", unix_time() as i64)
-        .execute(&self.inner)
-        .await
-        .map_err(|err| {
-            tracing::error!("SQLite could not insert payment ID: {}", err);
-            err
-        })?;
-
-        Ok(())
-    }
-
-    #[instrument(skip_all)]
-    async fn increment_mint_quote_amount_issued(
-        &mut self,
-        quote: &mut Acquired<mint::MintQuote>,
-        amount_issued: Amount,
-    ) -> Result<(), Self::Err> {
-        let new_amount_issued = quote
-            .increment_amount_issued(amount_issued)
-            .map_err(|_| Error::AmountOverflow)?;
-
-        // Update the amount_issued
-        query(
-            r#"
-            UPDATE mint_quote
-            SET amount_issued = :amount_issued
-            WHERE id = :quote_id
-            "#,
-        )?
-        .bind("amount_issued", new_amount_issued.to_i64())
-        .bind("quote_id", quote.id.to_string())
-        .execute(&self.inner)
-        .await
-        .inspect_err(|err| {
-            tracing::error!("SQLite could not update mint quote amount_issued: {}", err);
-        })?;
-
-        let current_time = unix_time();
-
-        query(
-            r#"
-INSERT INTO mint_quote_issued
-(quote_id, amount, timestamp)
-VALUES (:quote_id, :amount, :timestamp);
-            "#,
-        )?
-        .bind("quote_id", quote.id.to_string())
-        .bind("amount", amount_issued.to_i64())
-        .bind("timestamp", current_time as i64)
-        .execute(&self.inner)
-        .await?;
-
         Ok(())
     }
 

+ 2 - 3
crates/cdk/src/mint/issue/mod.rs

@@ -675,9 +675,8 @@ impl Mint {
             .await?;
 
 
-            tx
-            .increment_mint_quote_amount_issued(&mut mint_quote, amount_issued)
-            .await?;
+        mint_quote.add_issuance(amount_issued)?;
+        tx.update_mint_quote(&mut mint_quote).await?;
 
 
         // Mint operations have no input fees (no proofs being spent)

+ 5 - 11
crates/cdk/src/mint/ln.rs

@@ -6,7 +6,7 @@ use cdk_common::common::PaymentProcessorKey;
 use cdk_common::database::DynMintDatabase;
 use cdk_common::mint::MintQuote;
 use cdk_common::payment::DynMintPayment;
-use cdk_common::{database, Amount, MintQuoteState, PaymentMethod};
+use cdk_common::{Amount, MintQuoteState, PaymentMethod};
 use tracing::instrument;
 
 use super::subscription::PubSubManager;
@@ -82,27 +82,21 @@ impl Mint {
 
                 let amount_paid = to_unit(payment.payment_amount, &payment.unit, &new_quote.unit)?;
 
-                match tx
-                    .increment_mint_quote_amount_paid(
-                        &mut new_quote,
-                        amount_paid,
-                        payment.payment_id.clone(),
-                    )
-                    .await
-                {
+                match new_quote.add_payment(amount_paid, payment.payment_id.clone(), None) {
                     Ok(()) => {
+                        tx.update_mint_quote(&mut new_quote).await?;
                         if let Some(pubsub_manager) = pubsub_manager.as_ref() {
                             pubsub_manager.mint_quote_payment(&new_quote, new_quote.amount_paid());
                         }
                     }
-                    Err(database::Error::Duplicate) => {
+                    Err(crate::Error::DuplicatePaymentId) => {
                         tracing::debug!(
                             "Payment ID {} already processed (caught race condition in check_mint_quote_paid)",
                             payment.payment_id
                         );
                         // This is fine - another concurrent request already processed this payment
                     }
-                    Err(e) => return Err(e.into()),
+                    Err(e) => return Err(e),
                 }
             }
         }

+ 2 - 6
crates/cdk/src/mint/melt/melt_saga/mod.rs

@@ -556,12 +556,8 @@ impl MeltSaga<SetupComplete> {
         )
         .await?;
 
-        tx.increment_mint_quote_amount_paid(
-            &mut mint_quote,
-            amount,
-            self.state_data.quote.id.to_string(),
-        )
-        .await?;
+        mint_quote.add_payment(amount, self.state_data.quote.id.to_string(), None)?;
+        tx.update_mint_quote(&mut mint_quote).await?;
 
         self.pubsub
             .mint_quote_payment(&mint_quote, mint_quote.amount_paid());

+ 8 - 10
crates/cdk/src/mint/mod.rs

@@ -749,25 +749,23 @@ impl Mint {
                     payment_amount_quote_unit
                 );
 
-                match tx
-                    .increment_mint_quote_amount_paid(
-                        mint_quote,
-                        payment_amount_quote_unit,
-                        wait_payment_response.payment_id.clone(),
-                    )
-                    .await
-                {
+                match mint_quote.add_payment(
+                    payment_amount_quote_unit,
+                    wait_payment_response.payment_id.clone(),
+                    None,
+                ) {
                     Ok(()) => {
+                        tx.update_mint_quote(mint_quote).await?;
                         pubsub_manager.mint_quote_payment(mint_quote, mint_quote.amount_paid());
                     }
-                    Err(database::Error::Duplicate) => {
+                    Err(Error::DuplicatePaymentId) => {
                         tracing::info!(
                             "Payment ID {} already processed (caught race condition)",
                             wait_payment_response.payment_id
                         );
                         // This is fine - another concurrent request already processed this payment
                     }
-                    Err(e) => return Err(e.into()),
+                    Err(e) => return Err(e),
                 }
             }
         } else {