1use crate::error::{Result, Web4Error};
18use serde::{Deserialize, Serialize};
19
20#[derive(Clone, Debug, Serialize, Deserialize)]
22pub struct ATPAccount {
23 pub available: f64,
25 pub locked: f64,
27 pub adp: f64,
29 pub initial_balance: f64,
31}
32
33impl ATPAccount {
34 pub fn new(initial: f64) -> Self {
35 Self {
36 available: initial,
37 locked: 0.0,
38 adp: 0.0,
39 initial_balance: initial,
40 }
41 }
42
43 pub fn total(&self) -> f64 {
45 self.available + self.locked
46 }
47
48 pub fn energy_ratio(&self) -> f64 {
51 let total = self.total() + self.adp;
52 if total == 0.0 {
53 0.5
54 } else {
55 self.total() / total
56 }
57 }
58
59 pub fn lock(&mut self, amount: f64) -> Result<()> {
61 if amount < 0.0 {
62 return Err(Web4Error::InvalidInput("Lock amount must be non-negative".into()));
63 }
64 if self.available < amount {
65 return Err(Web4Error::InvalidInput(format!(
66 "Insufficient available ATP: {} < {}",
67 self.available, amount
68 )));
69 }
70 self.available -= amount;
71 self.locked += amount;
72 Ok(())
73 }
74
75 pub fn commit(&mut self, amount: f64) -> Result<f64> {
77 if amount < 0.0 {
78 return Err(Web4Error::InvalidInput("Commit amount must be non-negative".into()));
79 }
80 let actual = amount.min(self.locked);
81 self.locked -= actual;
82 self.adp += actual;
83 Ok(actual)
84 }
85
86 pub fn rollback(&mut self, amount: f64) -> Result<f64> {
88 if amount < 0.0 {
89 return Err(Web4Error::InvalidInput("Rollback amount must be non-negative".into()));
90 }
91 let actual = amount.min(self.locked);
92 self.locked -= actual;
93 self.available += actual;
94 Ok(actual)
95 }
96
97 pub fn recharge(&mut self, rate: f64, max_multiplier: f64) -> f64 {
100 let max_balance = self.initial_balance * max_multiplier;
101 let raw_recharge = self.initial_balance * rate;
102 let space = (max_balance - self.total()).max(0.0);
103 let actual = raw_recharge.min(space);
104 self.available += actual;
105 actual
106 }
107}
108
109impl Default for ATPAccount {
110 fn default() -> Self {
111 Self::new(100.0)
112 }
113}
114
115#[derive(Clone, Debug, Serialize, Deserialize)]
117pub struct TransferResult {
118 pub fee: f64,
120 pub sender_balance: f64,
122 pub receiver_balance: f64,
124 pub actual_credit: f64,
126 pub overflow: f64,
128}
129
130pub fn transfer(
137 sender: &mut ATPAccount,
138 receiver: &mut ATPAccount,
139 amount: f64,
140 fee_rate: f64,
141 max_balance: Option<f64>,
142) -> Result<TransferResult> {
143 if amount < 0.0 {
144 return Err(Web4Error::InvalidInput("Transfer amount must be non-negative".into()));
145 }
146
147 let fee = amount * fee_rate;
148 let total_deduction = amount + fee;
149
150 if sender.available < total_deduction {
151 return Err(Web4Error::InvalidInput(format!(
152 "Insufficient ATP: {} < {} (amount {} + fee {})",
153 sender.available, total_deduction, amount, fee
154 )));
155 }
156
157 let (actual_credit, overflow) = if let Some(max) = max_balance {
158 let space = (max - receiver.available).max(0.0);
159 let credit = amount.min(space);
160 (credit, amount - credit)
161 } else {
162 (amount, 0.0)
163 };
164
165 sender.available -= total_deduction;
166 sender.available += overflow; receiver.available += actual_credit;
168
169 Ok(TransferResult {
170 fee,
171 sender_balance: sender.available,
172 receiver_balance: receiver.available,
173 actual_credit,
174 overflow,
175 })
176}
177
178pub fn sliding_scale(
183 quality: f64,
184 base_payment: f64,
185 zero_threshold: f64,
186 full_threshold: f64,
187) -> f64 {
188 if quality < zero_threshold {
189 0.0
190 } else if quality >= full_threshold {
191 base_payment
192 } else {
193 base_payment * (quality - zero_threshold) / (full_threshold - zero_threshold)
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_account_lifecycle() {
203 let mut account = ATPAccount::new(100.0);
204 assert_eq!(account.total(), 100.0);
205 assert_eq!(account.energy_ratio(), 1.0); account.lock(30.0).unwrap();
209 assert_eq!(account.available, 70.0);
210 assert_eq!(account.locked, 30.0);
211 assert_eq!(account.total(), 100.0);
212
213 account.commit(20.0).unwrap();
215 assert_eq!(account.locked, 10.0);
216 assert_eq!(account.adp, 20.0);
217 assert_eq!(account.total(), 80.0);
218
219 assert!((account.energy_ratio() - 0.8).abs() < 1e-10);
221
222 account.rollback(10.0).unwrap();
224 assert_eq!(account.available, 80.0);
225 assert_eq!(account.locked, 0.0);
226 }
227
228 #[test]
229 fn test_transfer_conservation() {
230 let mut sender = ATPAccount::new(100.0);
231 let mut receiver = ATPAccount::new(50.0);
232
233 let result = transfer(&mut sender, &mut receiver, 30.0, 0.05, None).unwrap();
234
235 assert_eq!(result.fee, 1.5);
236 assert_eq!(result.actual_credit, 30.0);
237 assert_eq!(result.overflow, 0.0);
238 assert_eq!(sender.available, 68.5); assert_eq!(receiver.available, 80.0); }
244
245 #[test]
246 fn test_transfer_with_max_balance() {
247 let mut sender = ATPAccount::new(100.0);
248 let mut receiver = ATPAccount::new(90.0);
249
250 let result = transfer(&mut sender, &mut receiver, 30.0, 0.0, Some(100.0)).unwrap();
252
253 assert_eq!(result.actual_credit, 10.0);
254 assert_eq!(result.overflow, 20.0);
255 assert_eq!(sender.available, 90.0); assert_eq!(receiver.available, 100.0);
257 }
258
259 #[test]
260 fn test_recharge() {
261 let mut account = ATPAccount::new(100.0);
262 account.available = 50.0; let recharged = account.recharge(0.1, 3.0);
265 assert_eq!(recharged, 10.0); assert_eq!(account.available, 60.0);
267 }
268
269 #[test]
270 fn test_sliding_scale() {
271 assert_eq!(sliding_scale(0.1, 100.0, 0.3, 0.7), 0.0);
272 assert!((sliding_scale(0.5, 100.0, 0.3, 0.7) - 50.0).abs() < 1e-10);
273 assert_eq!(sliding_scale(0.8, 100.0, 0.3, 0.7), 100.0);
274 }
275
276 #[test]
277 fn test_zero_balance_energy_ratio() {
278 let account = ATPAccount::new(0.0);
279 assert_eq!(account.energy_ratio(), 0.5); }
281
282 #[test]
283 fn test_insufficient_balance() {
284 let mut sender = ATPAccount::new(10.0);
285 let mut receiver = ATPAccount::new(0.0);
286 let result = transfer(&mut sender, &mut receiver, 20.0, 0.0, None);
287 assert!(result.is_err());
288 }
289}