1use super::{
4 map_db_error, params_refs, parse_datetime_row, parse_enum_row, parse_uuid_row,
5 with_immediate_transaction,
6};
7use chrono::Utc;
8use r2d2::Pool;
9use r2d2_sqlite::SqliteConnectionManager;
10use rusqlite::OptionalExtension;
11use stateset_core::{
12 CommerceError, Result, X402Asset, X402CreditAccount, X402CreditAdjustment, X402CreditDirection,
13 X402CreditRepository, X402CreditTransaction, X402CreditTransactionFilter, X402Network,
14};
15use uuid::Uuid;
16
17#[derive(Debug)]
19pub struct SqliteX402CreditRepository {
20 pool: Pool<SqliteConnectionManager>,
21}
22
23impl SqliteX402CreditRepository {
24 #[must_use]
25 pub const fn new(pool: Pool<SqliteConnectionManager>) -> Self {
26 Self { pool }
27 }
28
29 fn conn(&self) -> Result<r2d2::PooledConnection<SqliteConnectionManager>> {
30 self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))
31 }
32
33 fn row_to_account(row: &rusqlite::Row<'_>) -> rusqlite::Result<X402CreditAccount> {
34 Ok(X402CreditAccount {
35 id: parse_uuid_row(&row.get::<_, String>("id")?, "x402_credit_account", "id")?,
36 payer_address: row.get("payer_address")?,
37 asset: parse_enum_row(&row.get::<_, String>("asset")?, "x402_credit_account", "asset")?,
38 network: parse_enum_row(
39 &row.get::<_, String>("network")?,
40 "x402_credit_account",
41 "network",
42 )?,
43 balance: row.get::<_, i64>("balance")? as u64,
44 created_at: parse_datetime_row(
45 &row.get::<_, String>("created_at")?,
46 "x402_credit_account",
47 "created_at",
48 )?,
49 updated_at: parse_datetime_row(
50 &row.get::<_, String>("updated_at")?,
51 "x402_credit_account",
52 "updated_at",
53 )?,
54 })
55 }
56
57 fn row_to_transaction(row: &rusqlite::Row<'_>) -> rusqlite::Result<X402CreditTransaction> {
58 Ok(X402CreditTransaction {
59 id: parse_uuid_row(&row.get::<_, String>("id")?, "x402_credit_tx", "id")?,
60 account_id: parse_uuid_row(
61 &row.get::<_, String>("account_id")?,
62 "x402_credit_tx",
63 "account_id",
64 )?,
65 payer_address: row.get("payer_address")?,
66 asset: parse_enum_row(&row.get::<_, String>("asset")?, "x402_credit_tx", "asset")?,
67 network: parse_enum_row(
68 &row.get::<_, String>("network")?,
69 "x402_credit_tx",
70 "network",
71 )?,
72 direction: parse_enum_row(
73 &row.get::<_, String>("direction")?,
74 "x402_credit_tx",
75 "direction",
76 )?,
77 amount: row.get::<_, i64>("amount")? as u64,
78 balance_after: row.get::<_, i64>("balance_after")? as u64,
79 reason: row.get("reason")?,
80 reference_id: row.get("reference_id")?,
81 metadata: row.get("metadata")?,
82 created_at: parse_datetime_row(
83 &row.get::<_, String>("created_at")?,
84 "x402_credit_tx",
85 "created_at",
86 )?,
87 })
88 }
89
90 fn insert_account_if_missing(
91 tx: &rusqlite::Transaction<'_>,
92 payer_address: &str,
93 asset: X402Asset,
94 network: X402Network,
95 ) -> std::result::Result<(Uuid, i64), rusqlite::Error> {
96 let asset_str = asset.to_string().to_lowercase();
97 let network_str = network.to_string();
98
99 let existing: Option<(String, i64)> = tx
100 .query_row(
101 "SELECT id, balance FROM x402_credit_accounts WHERE payer_address = ? AND asset = ? AND network = ?",
102 rusqlite::params![payer_address, asset_str, network_str],
103 |row| Ok((row.get(0)?, row.get(1)?)),
104 )
105 .optional()?;
106
107 if let Some((id, balance)) = existing {
108 let account_id = Uuid::parse_str(&id).map_err(|e| {
109 rusqlite::Error::ToSqlConversionFailure(Box::new(CommerceError::DatabaseError(
110 format!("Invalid UUID: {e}"),
111 )))
112 })?;
113 return Ok((account_id, balance));
114 }
115
116 let account_id = Uuid::new_v4();
117 let now = Utc::now().to_rfc3339();
118 tx.execute(
119 "INSERT INTO x402_credit_accounts (
120 id, payer_address, asset, network, balance, created_at, updated_at
121 ) VALUES (?, ?, ?, ?, ?, ?, ?)",
122 rusqlite::params![
123 account_id.to_string(),
124 payer_address,
125 asset_str,
126 network_str,
127 0i64,
128 now,
129 now,
130 ],
131 )?;
132
133 Ok((account_id, 0))
134 }
135}
136
137impl X402CreditRepository for SqliteX402CreditRepository {
138 fn get_account(
139 &self,
140 payer_address: &str,
141 asset: X402Asset,
142 network: X402Network,
143 ) -> Result<Option<X402CreditAccount>> {
144 let conn = self.conn()?;
145 let asset_str = asset.to_string().to_lowercase();
146 let network_str = network.to_string();
147
148 let mut stmt = conn
149 .prepare(
150 "SELECT * FROM x402_credit_accounts WHERE payer_address = ? AND asset = ? AND network = ?",
151 )
152 .map_err(map_db_error)?;
153
154 stmt.query_row(
155 rusqlite::params![payer_address, asset_str, network_str],
156 Self::row_to_account,
157 )
158 .optional()
159 .map_err(map_db_error)
160 }
161
162 fn get_or_create_account(
163 &self,
164 payer_address: &str,
165 asset: X402Asset,
166 network: X402Network,
167 ) -> Result<X402CreditAccount> {
168 let conn = self.conn()?;
169 let now = Utc::now().to_rfc3339();
170 let account_id = Uuid::new_v4();
171
172 conn.execute(
173 "INSERT OR IGNORE INTO x402_credit_accounts (
174 id, payer_address, asset, network, balance, created_at, updated_at
175 ) VALUES (?, ?, ?, ?, ?, ?, ?)",
176 rusqlite::params![
177 account_id.to_string(),
178 payer_address,
179 asset.to_string().to_lowercase(),
180 network.to_string(),
181 0i64,
182 now,
183 now,
184 ],
185 )
186 .map_err(map_db_error)?;
187
188 self.get_account(payer_address, asset, network)?.ok_or(CommerceError::NotFound)
189 }
190
191 fn get_balance(
192 &self,
193 payer_address: &str,
194 asset: X402Asset,
195 network: X402Network,
196 ) -> Result<u64> {
197 let account = self.get_or_create_account(payer_address, asset, network)?;
198 Ok(account.balance)
199 }
200
201 fn adjust_balance(&self, input: X402CreditAdjustment) -> Result<X402CreditTransaction> {
202 let X402CreditAdjustment {
203 payer_address,
204 asset,
205 network,
206 direction,
207 amount,
208 reason,
209 reference_id,
210 metadata,
211 } = input;
212
213 let amount_i64 = i64::try_from(amount).map_err(|_| {
214 CommerceError::ValidationError("x402 credit amount exceeds i64 range".to_string())
215 })?;
216
217 with_immediate_transaction(&self.pool, |tx| {
218 let (account_id, current_balance) =
219 Self::insert_account_if_missing(tx, &payer_address, asset, network)?;
220
221 if current_balance < 0 {
222 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
223 CommerceError::DatabaseError(
224 "x402 credit balance negative in database".to_string(),
225 ),
226 )));
227 }
228
229 let new_balance = match input.direction {
230 X402CreditDirection::Credit => {
231 current_balance.checked_add(amount_i64).ok_or_else(|| {
232 rusqlite::Error::ToSqlConversionFailure(Box::new(
233 CommerceError::ValidationError("x402 balance overflow".to_string()),
234 ))
235 })?
236 }
237 X402CreditDirection::Debit => {
238 if current_balance < amount_i64 {
239 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
240 CommerceError::NotPermitted(
241 "Insufficient x402 credit balance".to_string(),
242 ),
243 )));
244 }
245 current_balance - amount_i64
246 }
247 _ => current_balance,
248 };
249
250 let now = Utc::now();
251 let now_str = now.to_rfc3339();
252
253 tx.execute(
254 "UPDATE x402_credit_accounts SET balance = ?, updated_at = ? WHERE id = ?",
255 rusqlite::params![new_balance, now_str, account_id.to_string()],
256 )?;
257
258 let tx_id = Uuid::new_v4();
259 let asset_str = asset.to_string().to_lowercase();
260 let network_str = network.to_string();
261 let direction_str = direction.to_string();
262
263 tx.execute(
264 "INSERT INTO x402_credit_transactions (
265 id, account_id, payer_address, asset, network, direction,
266 amount, balance_after, reason, reference_id, metadata, created_at
267 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
268 rusqlite::params![
269 tx_id.to_string(),
270 account_id.to_string(),
271 payer_address.as_str(),
272 asset_str,
273 network_str,
274 direction_str,
275 amount_i64,
276 new_balance,
277 reason.as_deref(),
278 reference_id.as_deref(),
279 metadata.as_deref(),
280 now_str,
281 ],
282 )?;
283
284 Ok(X402CreditTransaction {
285 id: tx_id,
286 account_id,
287 payer_address: payer_address.clone(),
288 asset,
289 network,
290 direction,
291 amount,
292 balance_after: new_balance as u64,
293 reason: reason.clone(),
294 reference_id: reference_id.clone(),
295 metadata: metadata.clone(),
296 created_at: now,
297 })
298 })
299 }
300
301 fn list_transactions(
302 &self,
303 filter: X402CreditTransactionFilter,
304 ) -> Result<Vec<X402CreditTransaction>> {
305 let conn = self.conn()?;
306 let mut conditions: Vec<String> = Vec::new();
307 let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
308
309 if let Some(payer) = filter.payer_address.as_ref() {
310 conditions.push("payer_address = ?".to_string());
311 params.push(Box::new(payer.clone()));
312 }
313 if let Some(asset) = filter.asset {
314 conditions.push("asset = ?".to_string());
315 params.push(Box::new(asset.to_string().to_lowercase()));
316 }
317 if let Some(network) = filter.network {
318 conditions.push("network = ?".to_string());
319 params.push(Box::new(network.to_string()));
320 }
321 if let Some(direction) = filter.direction {
322 conditions.push("direction = ?".to_string());
323 params.push(Box::new(direction.to_string()));
324 }
325
326 let mut sql = "SELECT * FROM x402_credit_transactions".to_string();
327 if !conditions.is_empty() {
328 sql.push_str(" WHERE ");
329 sql.push_str(&conditions.join(" AND "));
330 }
331 sql.push_str(" ORDER BY created_at DESC");
332
333 if let Some(limit) = filter.limit {
334 sql.push_str(" LIMIT ?");
335 params.push(Box::new(i64::from(limit)));
336 }
337 if let Some(offset) = filter.offset {
338 sql.push_str(" OFFSET ?");
339 params.push(Box::new(i64::from(offset)));
340 }
341
342 let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
343 let param_refs = params_refs(¶ms);
344 let rows = stmt
345 .query_map(param_refs.as_slice(), Self::row_to_transaction)
346 .map_err(map_db_error)?;
347
348 let mut results = Vec::new();
349 for row in rows {
350 results.push(row.map_err(map_db_error)?);
351 }
352 Ok(results)
353 }
354}