inflight_hold.rs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. //! Authorize a payment hold, capture it in parts over time, then void the rest.
  2. //!
  3. //! This mirrors a card-style authorization: the customer's funds are parked in
  4. //! per-destination holding accounts up front, captured (confirmed) in slices as
  5. //! goods ship, and whatever is never captured is released (voided) back to the
  6. //! customer. The `sleep` calls stand in for the real time that passes between
  7. //! authorization, each capture, and the final release.
  8. //!
  9. //! Run with:
  10. //! ```sh
  11. //! cargo run -p kuatia --example inflight_hold
  12. //! ```
  13. use std::sync::Arc;
  14. use std::time::Duration;
  15. use kuatia::prelude::*;
  16. use kuatia_storage_sql::SqlStore;
  17. use tokio::time::sleep;
  18. #[tokio::main]
  19. async fn main() -> Result<(), Box<dyn std::error::Error>> {
  20. let ledger = connect().await?;
  21. let customer = AccountId::new(1);
  22. let merchant = AccountId::new(2);
  23. let fee = AccountId::new(3);
  24. let external = AccountId::new(99);
  25. let usd = AssetId::new(1);
  26. let money = Amount::new(2); // two-decimal money
  27. ledger
  28. .create_account(Account::debit_must_not_exceed_credit(customer))
  29. .await?;
  30. ledger
  31. .create_account(Account::debit_must_not_exceed_credit(merchant))
  32. .await?;
  33. // The fee account collects the processing fee; a system account has no floor.
  34. ledger.create_account(Account::new(fee)).await?;
  35. ledger.create_account(Account::new(external)).await?;
  36. // Fund the customer with $100.00.
  37. ledger
  38. .commit(
  39. TransferBuilder::new()
  40. .deposit(customer, usd, money.parse("100.00")?, external)?
  41. .build(),
  42. )
  43. .await?;
  44. println!("funded:");
  45. print_balances(&ledger, &money, customer, merchant, fee, usd).await?;
  46. // Authorize a $100 order: $90 destined for the merchant, $10 for the fee.
  47. // The funds leave the customer and park in one holding account per
  48. // destination. Nothing has reached the merchant or fee account yet.
  49. let order = TransferBuilder::new()
  50. .pay(customer, merchant, usd, money.parse("90.00")?)
  51. .pay(customer, fee, usd, money.parse("10.00")?)
  52. .build();
  53. let auth = ledger.authorize(order).await?;
  54. println!("\nauthorized (funds now held):");
  55. print_balances(&ledger, &money, customer, merchant, fee, usd).await?;
  56. print_status(&ledger, &money, &auth.inflight).await?;
  57. // Time passes before the first shipment.
  58. sleep(Duration::from_millis(300)).await;
  59. // First partial capture: ship $40 of goods, so capture $40 to the merchant.
  60. // The remaining $50 of the merchant hold stays parked.
  61. println!("\ncapture #1: $40.00 to the merchant");
  62. ledger
  63. .confirm(
  64. &auth.inflight,
  65. confirm_one(customer, merchant, usd, money.parse("40.00")?),
  66. )
  67. .await?;
  68. print_status(&ledger, &money, &auth.inflight).await?;
  69. // More time passes before the second shipment.
  70. sleep(Duration::from_millis(300)).await;
  71. // Second partial capture: ship another $20 to the merchant and take the full
  72. // $10 processing fee. The fee hold drains and closes; the merchant hold still
  73. // has $30 parked.
  74. println!("\ncapture #2: $20.00 to the merchant, $10.00 to the fee account");
  75. ledger
  76. .confirm(
  77. &auth.inflight,
  78. TransferBuilder::new()
  79. .pay(customer, merchant, usd, money.parse("20.00")?)
  80. .pay(customer, fee, usd, money.parse("10.00")?)
  81. .build(),
  82. )
  83. .await?;
  84. print_status(&ledger, &money, &auth.inflight).await?;
  85. // The order is finalized before everything was captured.
  86. sleep(Duration::from_millis(300)).await;
  87. // Void the remainder: the merchant hold's uncaptured $30 returns to the
  88. // customer. Already-captured amounts are untouched.
  89. println!("\nvoid: release the uncaptured remainder back to the customer");
  90. ledger.void(&auth.inflight).await?;
  91. print_status(&ledger, &money, &auth.inflight).await?;
  92. // Final tally: merchant captured $60, fee $10, customer got the $30 back.
  93. println!("\nfinal balances:");
  94. print_balances(&ledger, &money, customer, merchant, fee, usd).await?;
  95. Ok(())
  96. }
  97. /// A one-leg confirm set, built with the same `.pay()` shape as a transfer:
  98. /// `from` is the leg's funder, `to` its destination.
  99. fn confirm_one(from: AccountId, to: AccountId, asset: AssetId, amount: Cent) -> Transfer {
  100. TransferBuilder::new().pay(from, to, asset, amount).build()
  101. }
  102. async fn print_balances(
  103. ledger: &Arc<Ledger>,
  104. money: &Amount,
  105. customer: AccountId,
  106. merchant: AccountId,
  107. fee: AccountId,
  108. usd: AssetId,
  109. ) -> Result<(), Box<dyn std::error::Error>> {
  110. println!(
  111. " customer: {} merchant: {} fee: {}",
  112. money.format(ledger.balance(&customer, &usd).await?),
  113. money.format(ledger.balance(&merchant, &usd).await?),
  114. money.format(ledger.balance(&fee, &usd).await?),
  115. );
  116. Ok(())
  117. }
  118. async fn print_status(
  119. ledger: &Arc<Ledger>,
  120. money: &Amount,
  121. inflight: &EnvelopeId,
  122. ) -> Result<(), Box<dyn std::error::Error>> {
  123. let status = ledger.inflight_status(inflight).await?;
  124. println!(" state: {:?}", status.state);
  125. for leg in &status.legs {
  126. println!(
  127. " -> {:?}: authorized {}, confirmed {}, voided {}, held {}",
  128. leg.destination,
  129. money.format(leg.authorized),
  130. money.format(leg.confirmed),
  131. money.format(leg.voided),
  132. money.format(leg.held),
  133. );
  134. }
  135. Ok(())
  136. }
  137. /// Open a fresh in-memory SQLite database, run migrations, and wrap it in a
  138. /// `Ledger`. Point the connection string at a file or a Postgres URL for a
  139. /// persistent ledger.
  140. async fn connect() -> Result<Arc<Ledger>, Box<dyn std::error::Error>> {
  141. sqlx::any::install_default_drivers();
  142. let pool = sqlx::any::AnyPoolOptions::new()
  143. .max_connections(1)
  144. .connect("sqlite::memory:")
  145. .await?;
  146. let store = SqlStore::new(pool);
  147. store.migrate().await?;
  148. let ledger = Arc::new(Ledger::new(store));
  149. // On startup, finish any commit a crash interrupted (idempotent roll-forward).
  150. ledger.recover().await?;
  151. Ok(ledger)
  152. }