use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::services::{CreditBalance, CreditHistory, CreditHistoryItem, HoldResult, SpendResult};
fn default_currency() -> String {
"SOL".to_string()
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SpendCreditsRequest {
pub amount_lamports: i64,
#[serde(default = "default_currency")]
pub currency: String,
pub idempotency_key: String,
pub reference_type: String,
pub reference_id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SpendCreditsResponse {
pub transaction_id: Uuid,
pub new_balance_lamports: i64,
pub amount_lamports: i64,
pub currency: String,
pub display: String,
}
impl SpendCreditsResponse {
pub fn from_result(result: SpendResult, currency: &str) -> Self {
let display = format_balance(result.new_balance_lamports, currency);
Self {
transaction_id: result.transaction_id,
new_balance_lamports: result.new_balance_lamports,
amount_lamports: result.amount_lamports,
currency: currency.to_string(),
display,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateHoldRequest {
pub amount_lamports: i64,
#[serde(default = "default_currency")]
pub currency: String,
pub idempotency_key: String,
#[serde(default = "default_ttl")]
pub ttl_minutes: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub reference_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reference_id: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
fn default_ttl() -> i64 {
15
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateHoldResponse {
pub hold_id: Uuid,
pub is_new: bool,
pub amount_lamports: i64,
pub expires_at: DateTime<Utc>,
pub currency: String,
}
impl CreateHoldResponse {
pub fn from_result(result: HoldResult, currency: &str) -> Self {
Self {
hold_id: result.hold_id,
is_new: result.is_new,
amount_lamports: result.amount_lamports,
expires_at: result.expires_at,
currency: currency.to_string(),
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CaptureHoldResponse {
pub transaction_id: Uuid,
pub new_balance_lamports: i64,
pub amount_lamports: i64,
pub currency: String,
pub display: String,
}
impl CaptureHoldResponse {
pub fn from_result(result: SpendResult) -> Self {
let display = format_balance(result.new_balance_lamports, &result.currency);
Self {
transaction_id: result.transaction_id,
new_balance_lamports: result.new_balance_lamports,
amount_lamports: result.amount_lamports,
currency: result.currency,
display,
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReleaseHoldResponse {
pub released: bool,
pub message: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RefundRequestInput {
pub amount_lamports: i64,
pub transaction_id: Uuid,
pub reason: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RefundRequestResponse {
pub submitted: bool,
pub message: String,
pub request_id: Uuid,
}
use crate::repositories::UserCreditStats;
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreditUsageResponse {
pub total_deposited_lamports: i64,
pub total_spent_lamports: i64,
pub total_refunds_lamports: i64,
pub current_balance_lamports: i64,
pub deposit_count: u64,
pub spend_count: u64,
pub currency: String,
pub total_deposited_display: String,
pub total_spent_display: String,
pub current_balance_display: String,
}
impl From<UserCreditStats> for CreditUsageResponse {
fn from(stats: UserCreditStats) -> Self {
let display = |lamports: i64, currency: &str| -> String {
match currency {
"SOL" => {
let sol = lamports as f64 / 1_000_000_000.0;
format!("{:.4} SOL", sol)
}
_ => format!("{} {}", lamports, currency),
}
};
Self {
total_deposited_lamports: stats.total_deposited,
total_spent_lamports: stats.total_spent,
total_refunds_lamports: stats.total_refunds,
current_balance_lamports: stats.current_balance,
deposit_count: stats.deposit_count,
spend_count: stats.spend_count,
total_deposited_display: display(stats.total_deposited, &stats.currency),
total_spent_display: display(stats.total_spent, &stats.currency),
current_balance_display: display(stats.current_balance, &stats.currency),
currency: stats.currency,
}
}
}
fn format_balance(lamports: i64, currency: &str) -> String {
match currency {
"SOL" => {
let sol = lamports as f64 / 1_000_000_000.0;
format!("{:.4} SOL", sol)
}
"USD" => {
let usd = lamports as f64 / 1_000_000.0;
format!("${:.2}", usd)
}
_ => format!("{} {}", lamports, currency),
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreditBalanceResponse {
pub balance_lamports: i64,
pub currency: String,
pub display: String,
}
impl From<CreditBalance> for CreditBalanceResponse {
fn from(balance: CreditBalance) -> Self {
Self {
balance_lamports: balance.balance_lamports,
currency: balance.currency,
display: balance.display,
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BalancesResponse {
pub balances: Vec<CreditBalanceResponse>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreditTransactionResponse {
pub id: Uuid,
pub amount_lamports: i64,
pub currency: String,
pub tx_type: String,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub deposit_session_id: Option<Uuid>,
pub created_at: DateTime<Utc>,
}
impl From<CreditHistoryItem> for CreditTransactionResponse {
fn from(item: CreditHistoryItem) -> Self {
let description = match item.tx_type.as_str() {
"deposit" => "Privacy Cash deposit".to_string(),
"spend" => "Service usage".to_string(),
"adjustment" => "Manual adjustment".to_string(),
_ => format!("Unknown ({})", item.tx_type),
};
Self {
id: item.id,
amount_lamports: item.amount_lamports,
currency: item.currency,
tx_type: item.tx_type,
description,
deposit_session_id: item.deposit_session_id,
created_at: item.created_at,
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreditHistoryResponse {
pub transactions: Vec<CreditTransactionResponse>,
pub total: u64,
pub limit: u32,
pub offset: u32,
}
impl From<CreditHistory> for CreditHistoryResponse {
fn from(history: CreditHistory) -> Self {
Self {
transactions: history.items.into_iter().map(Into::into).collect(),
total: history.total,
limit: history.limit,
offset: history.offset,
}
}
}
use crate::repositories::CreditHoldEntity;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PendingHoldResponse {
pub hold_id: Uuid,
pub amount_lamports: i64,
pub currency: String,
pub expires_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reference_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reference_id: Option<Uuid>,
pub created_at: DateTime<Utc>,
}
impl From<CreditHoldEntity> for PendingHoldResponse {
fn from(hold: CreditHoldEntity) -> Self {
Self {
hold_id: hold.id,
amount_lamports: hold.amount,
currency: hold.currency,
expires_at: hold.expires_at,
reference_type: hold.reference_type,
reference_id: hold.reference_id,
created_at: hold.created_at,
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PendingHoldsResponse {
pub holds: Vec<PendingHoldResponse>,
pub total_held_lamports: i64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_balance_response_serialization() {
let response = CreditBalanceResponse {
balance_lamports: 1_000_000_000,
currency: "SOL".to_string(),
display: "1.0000 SOL".to_string(),
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"balanceLamports\":1000000000"));
assert!(json.contains("\"currency\":\"SOL\""));
assert!(json.contains("\"display\":\"1.0000 SOL\""));
}
#[test]
fn test_transaction_response_serialization() {
let response = CreditTransactionResponse {
id: Uuid::nil(),
amount_lamports: 500_000_000,
currency: "SOL".to_string(),
tx_type: "deposit".to_string(),
description: "Privacy Cash deposit".to_string(),
deposit_session_id: Some(Uuid::nil()),
created_at: Utc::now(),
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"txType\":\"deposit\""));
assert!(json.contains("\"description\":\"Privacy Cash deposit\""));
}
}