use async_trait::async_trait;
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::errors::AppError;
#[derive(Debug, Clone)]
pub struct TreasuryConfigEntity {
pub id: Uuid,
pub org_id: Option<Uuid>,
pub treasury_user_id: Uuid,
pub wallet_address: String,
pub encrypted_private_key: String,
pub encryption_key_id: String,
pub authorized_at: DateTime<Utc>,
pub authorized_by: Uuid,
}
impl TreasuryConfigEntity {
pub fn new(
org_id: Option<Uuid>,
treasury_user_id: Uuid,
wallet_address: String,
encrypted_private_key: String,
encryption_key_id: String,
authorized_by: Uuid,
) -> Self {
Self {
id: Uuid::new_v4(),
org_id,
treasury_user_id,
wallet_address,
encrypted_private_key,
encryption_key_id,
authorized_at: Utc::now(),
authorized_by,
}
}
}
#[async_trait]
pub trait TreasuryConfigRepository: Send + Sync {
async fn create(&self, config: TreasuryConfigEntity) -> Result<TreasuryConfigEntity, AppError>;
async fn find_by_id(&self, id: Uuid) -> Result<Option<TreasuryConfigEntity>, AppError>;
async fn find_by_org(&self, org_id: Uuid) -> Result<Option<TreasuryConfigEntity>, AppError>;
async fn find_global(&self) -> Result<Option<TreasuryConfigEntity>, AppError>;
async fn find_for_org(
&self,
org_id: Option<Uuid>,
) -> Result<Option<TreasuryConfigEntity>, AppError>;
async fn delete(&self, id: Uuid) -> Result<bool, AppError>;
async fn delete_by_org(&self, org_id: Option<Uuid>) -> Result<bool, AppError>;
}
pub struct InMemoryTreasuryConfigRepository {
configs: RwLock<HashMap<Uuid, TreasuryConfigEntity>>,
}
impl InMemoryTreasuryConfigRepository {
pub fn new() -> Self {
Self {
configs: RwLock::new(HashMap::new()),
}
}
}
impl Default for InMemoryTreasuryConfigRepository {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl TreasuryConfigRepository for InMemoryTreasuryConfigRepository {
async fn create(&self, config: TreasuryConfigEntity) -> Result<TreasuryConfigEntity, AppError> {
let mut configs = self.configs.write().await;
let has_duplicate = configs.values().any(|c| c.org_id == config.org_id);
if has_duplicate {
return Err(AppError::Validation(
"Treasury already configured for this org".into(),
));
}
configs.insert(config.id, config.clone());
Ok(config)
}
async fn find_by_id(&self, id: Uuid) -> Result<Option<TreasuryConfigEntity>, AppError> {
let configs = self.configs.read().await;
Ok(configs.get(&id).cloned())
}
async fn find_by_org(&self, org_id: Uuid) -> Result<Option<TreasuryConfigEntity>, AppError> {
let configs = self.configs.read().await;
Ok(configs.values().find(|c| c.org_id == Some(org_id)).cloned())
}
async fn find_global(&self) -> Result<Option<TreasuryConfigEntity>, AppError> {
let configs = self.configs.read().await;
Ok(configs.values().find(|c| c.org_id.is_none()).cloned())
}
async fn find_for_org(
&self,
org_id: Option<Uuid>,
) -> Result<Option<TreasuryConfigEntity>, AppError> {
if let Some(oid) = org_id {
if let Some(config) = self.find_by_org(oid).await? {
return Ok(Some(config));
}
}
self.find_global().await
}
async fn delete(&self, id: Uuid) -> Result<bool, AppError> {
let mut configs = self.configs.write().await;
Ok(configs.remove(&id).is_some())
}
async fn delete_by_org(&self, org_id: Option<Uuid>) -> Result<bool, AppError> {
let mut configs = self.configs.write().await;
let to_remove: Vec<Uuid> = configs
.iter()
.filter(|(_, c)| c.org_id == org_id)
.map(|(id, _)| *id)
.collect();
let removed = !to_remove.is_empty();
for id in to_remove {
configs.remove(&id);
}
Ok(removed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_create_and_find() {
let repo = InMemoryTreasuryConfigRepository::new();
let config = TreasuryConfigEntity::new(
None, Uuid::new_v4(),
"Treasury1Address".to_string(),
"encrypted_key".to_string(),
"v1".to_string(),
Uuid::new_v4(),
);
let config_id = config.id;
let created = repo.create(config).await.unwrap();
assert_eq!(created.id, config_id);
let found = repo.find_by_id(config_id).await.unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().wallet_address, "Treasury1Address");
}
#[tokio::test]
async fn test_find_global() {
let repo = InMemoryTreasuryConfigRepository::new();
let config = TreasuryConfigEntity::new(
None,
Uuid::new_v4(),
"GlobalTreasury".to_string(),
"encrypted_key".to_string(),
"v1".to_string(),
Uuid::new_v4(),
);
repo.create(config).await.unwrap();
let global = repo.find_global().await.unwrap();
assert!(global.is_some());
assert_eq!(global.unwrap().wallet_address, "GlobalTreasury");
}
#[tokio::test]
async fn test_find_for_org_with_fallback() {
let repo = InMemoryTreasuryConfigRepository::new();
let global = TreasuryConfigEntity::new(
None,
Uuid::new_v4(),
"GlobalTreasury".to_string(),
"encrypted_key".to_string(),
"v1".to_string(),
Uuid::new_v4(),
);
repo.create(global).await.unwrap();
let org_id = Uuid::new_v4();
let org_config = TreasuryConfigEntity::new(
Some(org_id),
Uuid::new_v4(),
"OrgTreasury".to_string(),
"encrypted_key".to_string(),
"v1".to_string(),
Uuid::new_v4(),
);
repo.create(org_config).await.unwrap();
let found = repo.find_for_org(Some(org_id)).await.unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().wallet_address, "OrgTreasury");
let other_org = Uuid::new_v4();
let found = repo.find_for_org(Some(other_org)).await.unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().wallet_address, "GlobalTreasury");
let found = repo.find_for_org(None).await.unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().wallet_address, "GlobalTreasury");
}
#[tokio::test]
async fn test_duplicate_org_rejected() {
let repo = InMemoryTreasuryConfigRepository::new();
let config1 = TreasuryConfigEntity::new(
None,
Uuid::new_v4(),
"Treasury1".to_string(),
"key1".to_string(),
"v1".to_string(),
Uuid::new_v4(),
);
repo.create(config1).await.unwrap();
let config2 = TreasuryConfigEntity::new(
None,
Uuid::new_v4(),
"Treasury2".to_string(),
"key2".to_string(),
"v1".to_string(),
Uuid::new_v4(),
);
let result = repo.create(config2).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_delete_by_org() {
let repo = InMemoryTreasuryConfigRepository::new();
let org_id = Uuid::new_v4();
let config = TreasuryConfigEntity::new(
Some(org_id),
Uuid::new_v4(),
"OrgTreasury".to_string(),
"key".to_string(),
"v1".to_string(),
Uuid::new_v4(),
);
repo.create(config).await.unwrap();
let deleted = repo.delete_by_org(Some(org_id)).await.unwrap();
assert!(deleted);
let found = repo.find_by_org(org_id).await.unwrap();
assert!(found.is_none());
}
}