pub mod accounts;
pub mod budget;
pub mod categories;
pub mod file_io;
pub mod income;
pub mod init;
pub mod payees;
pub mod targets;
pub mod transactions;
pub use accounts::AccountRepository;
pub use budget::BudgetRepository;
pub use categories::CategoryRepository;
pub use file_io::{read_json, write_json_atomic};
pub use income::IncomeRepository;
pub use init::initialize_storage;
pub use payees::PayeeRepository;
pub use targets::TargetRepository;
pub use transactions::TransactionRepository;
use std::path::{Path, PathBuf};
use crate::audit::{AuditEntry, AuditLogger, EntityType};
use crate::backup::{BackupManager, RestoreManager, RestoreResult};
use crate::config::paths::EnvelopePaths;
use crate::config::settings::BackupRetention;
use crate::error::{EnvelopeError, EnvelopeResult};
pub struct Storage {
paths: EnvelopePaths,
pub accounts: AccountRepository,
pub transactions: TransactionRepository,
pub categories: CategoryRepository,
pub budget: BudgetRepository,
pub payees: PayeeRepository,
pub targets: TargetRepository,
pub income: IncomeRepository,
audit: AuditLogger,
}
impl Storage {
pub fn new(paths: EnvelopePaths) -> Result<Self, EnvelopeError> {
paths.ensure_directories()?;
let audit = AuditLogger::new(paths.audit_log());
Ok(Self {
accounts: AccountRepository::new(paths.accounts_file()),
transactions: TransactionRepository::new(paths.transactions_file()),
categories: CategoryRepository::new(paths.budget_file()),
budget: BudgetRepository::new(paths.allocations_file()),
payees: PayeeRepository::new(paths.payees_file()),
targets: TargetRepository::new(paths.targets_file()),
income: IncomeRepository::new(paths.income_file()),
audit,
paths,
})
}
pub fn paths(&self) -> &EnvelopePaths {
&self.paths
}
pub fn audit(&self) -> &AuditLogger {
&self.audit
}
pub fn log_audit(&self, entry: &AuditEntry) -> EnvelopeResult<()> {
self.audit.log(entry)
}
pub fn log_create<T: serde::Serialize>(
&self,
entity_type: EntityType,
entity_id: impl Into<String>,
entity_name: Option<String>,
entity: &T,
) -> EnvelopeResult<()> {
let entry = AuditEntry::create(entity_type, entity_id, entity_name, entity);
self.audit.log(&entry)
}
pub fn log_update<T: serde::Serialize>(
&self,
entity_type: EntityType,
entity_id: impl Into<String>,
entity_name: Option<String>,
before: &T,
after: &T,
diff_summary: Option<String>,
) -> EnvelopeResult<()> {
let entry = AuditEntry::update(
entity_type,
entity_id,
entity_name,
before,
after,
diff_summary,
);
self.audit.log(&entry)
}
pub fn log_delete<T: serde::Serialize>(
&self,
entity_type: EntityType,
entity_id: impl Into<String>,
entity_name: Option<String>,
entity: &T,
) -> EnvelopeResult<()> {
let entry = AuditEntry::delete(entity_type, entity_id, entity_name, entity);
self.audit.log(&entry)
}
pub fn read_audit_log(&self, count: usize) -> EnvelopeResult<Vec<AuditEntry>> {
self.audit.read_recent(count)
}
pub fn load_all(&mut self) -> Result<(), EnvelopeError> {
self.accounts.load()?;
self.transactions.load()?;
self.categories.load()?;
self.budget.load()?;
self.payees.load()?;
self.targets.load()?;
self.income.load()?;
Ok(())
}
pub fn save_all(&self) -> Result<(), EnvelopeError> {
self.accounts.save()?;
self.transactions.save()?;
self.categories.save()?;
self.budget.save()?;
self.payees.save()?;
self.targets.save()?;
self.income.save()?;
Ok(())
}
pub fn is_initialized(&self) -> bool {
self.paths.settings_file().exists()
}
pub fn create_backup(&self) -> EnvelopeResult<PathBuf> {
let retention = BackupRetention::default();
let manager = BackupManager::new(self.paths.clone(), retention);
manager.create_backup()
}
pub fn create_backup_with_retention(
&self,
retention: BackupRetention,
) -> EnvelopeResult<(PathBuf, Vec<PathBuf>)> {
let manager = BackupManager::new(self.paths.clone(), retention);
manager.create_backup_with_retention()
}
pub fn restore_from_backup(&mut self, backup_path: &Path) -> EnvelopeResult<RestoreResult> {
let restore_manager = RestoreManager::new(self.paths.clone());
let result = restore_manager.restore_from_file(backup_path)?;
self.load_all()?;
Ok(result)
}
pub fn backup_manager(&self, retention: BackupRetention) -> BackupManager {
BackupManager::new(self.paths.clone(), retention)
}
pub fn backup_before_destructive(&self) -> EnvelopeResult<Option<PathBuf>> {
let retention = BackupRetention::default();
let manager = BackupManager::new(self.paths.clone(), retention);
if let Some(latest) = manager.get_latest_backup()? {
let age = chrono::Utc::now().signed_duration_since(latest.created_at);
if age.num_seconds() < 60 {
return Ok(None);
}
}
let path = manager.create_backup()?;
Ok(Some(path))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_storage_creation() {
let temp_dir = TempDir::new().unwrap();
let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
let storage = Storage::new(paths).unwrap();
assert!(temp_dir.path().join("data").exists());
assert!(temp_dir.path().join("backups").exists());
assert!(!storage.is_initialized());
}
}