inflight_hold.rs 6.2 KB

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