1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//! Credit system types and DTOs
//!
//! Contains data transfer objects for the credit service, including:
//! - Balance representations
//! - Transaction history items
//! - Operation results (spend, hold, adjust)
use uuid::Uuid;
use crate::repositories::{CreditBalanceEntity, CreditTransactionEntity};
/// Credit balance with formatted display
#[derive(Debug)]
pub struct CreditBalance {
/// Total balance in lamports
pub balance_lamports: i64,
/// Credits reserved by pending holds
pub held_lamports: i64,
/// Available balance (total - held)
pub available_lamports: i64,
/// Currency (e.g., "SOL")
pub currency: String,
/// User-friendly display (e.g., "0.5 SOL")
pub display: String,
}
impl CreditBalance {
pub(crate) fn from_entity(entity: CreditBalanceEntity) -> Self {
let available = entity.available();
// R2-M05: Use correct divisor and label based on currency
let display = match entity.currency.as_str() {
"SOL" => {
let sol_amount = available as f64 / 1_000_000_000.0;
format!("{:.4} SOL", sol_amount)
}
"USD" => {
let usd_amount = available as f64 / 1_000_000.0;
format!("${:.2}", usd_amount)
}
other => format!("{} {}", available, other),
};
Self {
balance_lamports: entity.balance,
held_lamports: entity.held_balance,
available_lamports: available,
currency: entity.currency,
display,
}
}
}
/// Credit transaction history item
#[derive(Debug)]
pub struct CreditHistoryItem {
pub id: Uuid,
pub amount_lamports: i64,
pub currency: String,
pub tx_type: String,
pub deposit_session_id: Option<Uuid>,
pub created_at: chrono::DateTime<chrono::Utc>,
}
impl From<CreditTransactionEntity> for CreditHistoryItem {
fn from(entity: CreditTransactionEntity) -> Self {
Self {
id: entity.id,
amount_lamports: entity.amount,
currency: entity.currency,
tx_type: entity.tx_type.as_str().to_string(),
deposit_session_id: entity.deposit_session_id,
created_at: entity.created_at,
}
}
}
/// Paginated transaction history
pub struct CreditHistory {
pub items: Vec<CreditHistoryItem>,
pub total: u64,
pub limit: u32,
pub offset: u32,
}
/// Result of a spend operation
#[derive(Debug)]
pub struct SpendResult {
/// Transaction ID
pub transaction_id: Uuid,
/// New balance after spend
pub new_balance_lamports: i64,
/// Amount spent
pub amount_lamports: i64,
/// S-14: Currency from the captured hold (avoids double-fetch in handler)
pub currency: String,
}
/// Result of a hold operation
#[derive(Debug)]
pub struct HoldResult {
/// Hold ID
pub hold_id: Uuid,
/// Whether this was a new hold or existing (idempotent)
pub is_new: bool,
/// Amount held
pub amount_lamports: i64,
/// When the hold expires
pub expires_at: chrono::DateTime<chrono::Utc>,
}
/// Result of an adjustment operation
#[derive(Debug)]
pub struct AdjustResult {
/// Transaction ID
pub transaction_id: Uuid,
/// New balance after adjustment
pub new_balance_lamports: i64,
/// Amount adjusted (positive = credit, negative = debit)
pub amount_lamports: i64,
}