diff --git a/crates/domain/src/ports/file_storage.rs b/crates/domain/src/ports/file_storage.rs new file mode 100644 index 0000000..f8d467c --- /dev/null +++ b/crates/domain/src/ports/file_storage.rs @@ -0,0 +1,20 @@ +use async_trait::async_trait; +use bytes::Bytes; +use crate::errors::DomainError; + +#[derive(Debug, Clone)] +pub struct FileEntry { + pub path: String, + pub size_bytes: u64, + pub is_directory: bool, +} + +#[async_trait] +pub trait FileStoragePort: Send + Sync { + async fn store_file(&self, path: &str, data: Bytes) -> Result<(), DomainError>; + async fn read_file(&self, path: &str) -> Result; + async fn delete_file(&self, path: &str) -> Result<(), DomainError>; + async fn list_directory(&self, path: &str) -> Result, DomainError>; + async fn file_exists(&self, path: &str) -> Result; + async fn available_space(&self) -> Result; +} diff --git a/crates/domain/src/ports/ingest_session_repo.rs b/crates/domain/src/ports/ingest_session_repo.rs new file mode 100644 index 0000000..de6cc3c --- /dev/null +++ b/crates/domain/src/ports/ingest_session_repo.rs @@ -0,0 +1,9 @@ +use async_trait::async_trait; +use crate::{entities::IngestSession, errors::DomainError, value_objects::SystemId}; + +#[async_trait] +pub trait IngestSessionRepository: Send + Sync { + async fn find_by_id(&self, id: &SystemId) -> Result, DomainError>; + async fn find_by_user(&self, user_id: &SystemId) -> Result, DomainError>; + async fn save(&self, session: &IngestSession) -> Result<(), DomainError>; +} diff --git a/crates/domain/src/ports/library_path_repo.rs b/crates/domain/src/ports/library_path_repo.rs new file mode 100644 index 0000000..874c2d3 --- /dev/null +++ b/crates/domain/src/ports/library_path_repo.rs @@ -0,0 +1,11 @@ +use async_trait::async_trait; +use crate::{entities::LibraryPath, errors::DomainError, value_objects::SystemId}; + +#[async_trait] +pub trait LibraryPathRepository: Send + Sync { + async fn find_by_id(&self, id: &SystemId) -> Result, DomainError>; + async fn find_by_volume(&self, volume_id: &SystemId) -> Result, DomainError>; + async fn find_ingest_destinations(&self, owner_id: &SystemId) -> Result, DomainError>; + async fn save(&self, path: &LibraryPath) -> Result<(), DomainError>; + async fn delete(&self, id: &SystemId) -> Result<(), DomainError>; +} diff --git a/crates/domain/src/ports/mod.rs b/crates/domain/src/ports/mod.rs index 5422bd6..c0a44d4 100644 --- a/crates/domain/src/ports/mod.rs +++ b/crates/domain/src/ports/mod.rs @@ -4,6 +4,11 @@ mod group_repo; mod role_repo; mod storage; mod user_repo; +mod storage_volume_repo; +mod library_path_repo; +mod ingest_session_repo; +mod quota_repo; +mod file_storage; pub use auth::{PasswordHasher, TokenIssuer}; pub use event_publisher::EventPublisher; @@ -11,3 +16,8 @@ pub use group_repo::GroupRepository; pub use role_repo::RoleRepository; pub use storage::{DataStream, StoragePort, StorageReader, StorageWriter}; pub use user_repo::UserRepository; +pub use storage_volume_repo::StorageVolumeRepository; +pub use library_path_repo::LibraryPathRepository; +pub use ingest_session_repo::IngestSessionRepository; +pub use quota_repo::{QuotaRepository, UsageLedgerRepository}; +pub use file_storage::{FileEntry, FileStoragePort}; diff --git a/crates/domain/src/ports/quota_repo.rs b/crates/domain/src/ports/quota_repo.rs new file mode 100644 index 0000000..8dd275d --- /dev/null +++ b/crates/domain/src/ports/quota_repo.rs @@ -0,0 +1,24 @@ +use async_trait::async_trait; +use crate::{ + entities::{QuotaDefinition, UsageLedgerEntry, UsageType}, + errors::DomainError, + value_objects::{DateTimeStamp, SystemId}, +}; + +#[async_trait] +pub trait QuotaRepository: Send + Sync { + async fn find_by_owner(&self, owner_id: &SystemId) -> Result, DomainError>; + async fn save(&self, quota: &QuotaDefinition) -> Result<(), DomainError>; + async fn delete(&self, id: &SystemId) -> Result<(), DomainError>; +} + +#[async_trait] +pub trait UsageLedgerRepository: Send + Sync { + async fn record(&self, entry: &UsageLedgerEntry) -> Result<(), DomainError>; + async fn sum_usage( + &self, + user_id: &SystemId, + usage_type: UsageType, + since: Option, + ) -> Result; +} diff --git a/crates/domain/src/ports/storage_volume_repo.rs b/crates/domain/src/ports/storage_volume_repo.rs new file mode 100644 index 0000000..96c86b5 --- /dev/null +++ b/crates/domain/src/ports/storage_volume_repo.rs @@ -0,0 +1,10 @@ +use async_trait::async_trait; +use crate::{entities::StorageVolume, errors::DomainError, value_objects::SystemId}; + +#[async_trait] +pub trait StorageVolumeRepository: Send + Sync { + async fn find_by_id(&self, id: &SystemId) -> Result, DomainError>; + async fn find_all(&self) -> Result, DomainError>; + async fn save(&self, volume: &StorageVolume) -> Result<(), DomainError>; + async fn delete(&self, id: &SystemId) -> Result<(), DomainError>; +} diff --git a/crates/domain/src/services/quota_checker.rs b/crates/domain/src/services/quota_checker.rs index bc7de8e..41fda49 100644 --- a/crates/domain/src/services/quota_checker.rs +++ b/crates/domain/src/services/quota_checker.rs @@ -1 +1,73 @@ -// Quota checker — will be implemented in Task 7 +use chrono::{Datelike, NaiveDate, TimeZone, Utc}; +use crate::entities::{QuotaDefinition, TimePeriod, UsageType}; +use crate::value_objects::DateTimeStamp; + +pub struct QuotaCheckResult { + pub allowed: bool, + pub current_usage: u64, + pub limit: u64, + pub is_unlimited: bool, +} + +pub fn check_quota( + quota: &QuotaDefinition, + usage_type: UsageType, + current_usage: u64, + requested_amount: u64, +) -> QuotaCheckResult { + if !quota.is_enforced { + return QuotaCheckResult { + allowed: true, + current_usage, + limit: 0, + is_unlimited: true, + }; + } + + let rule = quota.rules.iter().find(|r| r.dimension == usage_type); + let Some(rule) = rule else { + return QuotaCheckResult { + allowed: true, + current_usage, + limit: 0, + is_unlimited: true, + }; + }; + + if rule.is_unlimited { + return QuotaCheckResult { + allowed: true, + current_usage, + limit: 0, + is_unlimited: true, + }; + } + + QuotaCheckResult { + allowed: current_usage + requested_amount <= rule.limit_value, + current_usage, + limit: rule.limit_value, + is_unlimited: false, + } +} + +pub fn period_start(period: TimePeriod) -> Option { + let now = Utc::now(); + match period { + TimePeriod::Daily => { + let start = NaiveDate::from_ymd_opt(now.year(), now.month(), now.day()) + .expect("valid date") + .and_hms_opt(0, 0, 0) + .expect("valid time"); + Some(DateTimeStamp::from_datetime(Utc.from_utc_datetime(&start))) + } + TimePeriod::Monthly => { + let start = NaiveDate::from_ymd_opt(now.year(), now.month(), 1) + .expect("valid date") + .and_hms_opt(0, 0, 0) + .expect("valid time"); + Some(DateTimeStamp::from_datetime(Utc.from_utc_datetime(&start))) + } + TimePeriod::Lifetime => None, + } +} diff --git a/crates/domain/tests/services/mod.rs b/crates/domain/tests/services/mod.rs index b9bf335..be01fea 100644 --- a/crates/domain/tests/services/mod.rs +++ b/crates/domain/tests/services/mod.rs @@ -1 +1,2 @@ mod permission_service; +mod quota_checker; diff --git a/crates/domain/tests/services/quota_checker.rs b/crates/domain/tests/services/quota_checker.rs new file mode 100644 index 0000000..402a2db --- /dev/null +++ b/crates/domain/tests/services/quota_checker.rs @@ -0,0 +1,53 @@ +use domain::entities::{QuotaDefinition, TimePeriod, UsageType}; +use domain::services::quota_checker::check_quota; +use domain::value_objects::SystemId; + +fn make_quota(limit: u64) -> QuotaDefinition { + let mut q = QuotaDefinition::new(SystemId::new()); + q.add_rule(UsageType::StorageBytes, limit, TimePeriod::Monthly); + q +} + +#[test] +fn allows_within_limit() { + let q = make_quota(1000); + let r = check_quota(&q, UsageType::StorageBytes, 500, 400); + assert!(r.allowed); + assert!(!r.is_unlimited); + assert_eq!(r.limit, 1000); + assert_eq!(r.current_usage, 500); +} + +#[test] +fn rejects_over_limit() { + let q = make_quota(1000); + let r = check_quota(&q, UsageType::StorageBytes, 800, 300); + assert!(!r.allowed); + assert_eq!(r.limit, 1000); +} + +#[test] +fn unlimited_always_allowed() { + let mut q = QuotaDefinition::new(SystemId::new()); + q.add_unlimited_rule(UsageType::StorageBytes); + let r = check_quota(&q, UsageType::StorageBytes, u64::MAX, 1); + assert!(r.allowed); + assert!(r.is_unlimited); +} + +#[test] +fn unenforced_allows_all() { + let mut q = make_quota(100); + q.is_enforced = false; + let r = check_quota(&q, UsageType::StorageBytes, 9999, 9999); + assert!(r.allowed); + assert!(r.is_unlimited); +} + +#[test] +fn missing_rule_allows() { + let q = make_quota(1000); // rule for StorageBytes only + let r = check_quota(&q, UsageType::ApiCalls, 9999, 9999); + assert!(r.allowed); + assert!(r.is_unlimited); +}