use crate::audit::EntityType;
use crate::error::{EnvelopeError, EnvelopeResult};
use crate::models::{BudgetPeriod, IncomeExpectation, Money};
use crate::storage::Storage;
pub struct IncomeService<'a> {
storage: &'a Storage,
}
impl<'a> IncomeService<'a> {
pub fn new(storage: &'a Storage) -> Self {
Self { storage }
}
pub fn set_expected_income(
&self,
period: &BudgetPeriod,
amount: Money,
notes: Option<String>,
) -> EnvelopeResult<IncomeExpectation> {
if let Some(existing) = self.storage.income.get_for_period(period) {
let mut updated = existing.clone();
let before = existing.clone();
updated.set_expected_amount(amount);
if let Some(n) = notes {
updated.set_notes(n);
}
updated
.validate()
.map_err(|e| EnvelopeError::Validation(e.to_string()))?;
self.storage.income.upsert(updated.clone())?;
self.storage.income.save()?;
self.storage.log_update(
EntityType::IncomeExpectation,
updated.id.to_string(),
Some(format!("Income for {}", period)),
&before,
&updated,
Some(format!(
"{} -> {}",
before.expected_amount, updated.expected_amount
)),
)?;
Ok(updated)
} else {
let mut expectation = IncomeExpectation::new(period.clone(), amount);
if let Some(n) = notes {
expectation.set_notes(n);
}
expectation
.validate()
.map_err(|e| EnvelopeError::Validation(e.to_string()))?;
self.storage.income.upsert(expectation.clone())?;
self.storage.income.save()?;
self.storage.log_create(
EntityType::IncomeExpectation,
expectation.id.to_string(),
Some(format!("Income for {}", period)),
&expectation,
)?;
Ok(expectation)
}
}
pub fn get_expected_income(&self, period: &BudgetPeriod) -> Option<Money> {
self.storage
.income
.get_for_period(period)
.map(|e| e.expected_amount)
}
pub fn get_income_expectation(&self, period: &BudgetPeriod) -> Option<IncomeExpectation> {
self.storage.income.get_for_period(period)
}
pub fn delete_expected_income(&self, period: &BudgetPeriod) -> EnvelopeResult<bool> {
if let Some(removed) = self.storage.income.delete_for_period(period) {
self.storage.income.save()?;
self.storage.log_delete(
EntityType::IncomeExpectation,
removed.id.to_string(),
Some(format!("Income for {}", period)),
&removed,
)?;
Ok(true)
} else {
Ok(false)
}
}
pub fn get_all_expectations(&self) -> EnvelopeResult<Vec<IncomeExpectation>> {
self.storage.income.get_all()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::paths::EnvelopePaths;
use tempfile::TempDir;
fn create_test_storage() -> (TempDir, Storage) {
let temp_dir = TempDir::new().unwrap();
let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
let mut storage = Storage::new(paths).unwrap();
storage.load_all().unwrap();
(temp_dir, storage)
}
#[test]
fn test_set_expected_income() {
let (_temp_dir, storage) = create_test_storage();
let service = IncomeService::new(&storage);
let period = BudgetPeriod::monthly(2025, 1);
let expectation = service
.set_expected_income(&period, Money::from_cents(500000), None)
.unwrap();
assert_eq!(expectation.expected_amount.cents(), 500000);
assert_eq!(expectation.period, period);
}
#[test]
fn test_update_expected_income() {
let (_temp_dir, storage) = create_test_storage();
let service = IncomeService::new(&storage);
let period = BudgetPeriod::monthly(2025, 1);
service
.set_expected_income(&period, Money::from_cents(500000), None)
.unwrap();
let updated = service
.set_expected_income(
&period,
Money::from_cents(600000),
Some("Updated".to_string()),
)
.unwrap();
assert_eq!(updated.expected_amount.cents(), 600000);
assert_eq!(updated.notes, "Updated");
}
#[test]
fn test_get_expected_income() {
let (_temp_dir, storage) = create_test_storage();
let service = IncomeService::new(&storage);
let period = BudgetPeriod::monthly(2025, 1);
assert!(service.get_expected_income(&period).is_none());
service
.set_expected_income(&period, Money::from_cents(500000), None)
.unwrap();
let income = service.get_expected_income(&period).unwrap();
assert_eq!(income.cents(), 500000);
}
#[test]
fn test_delete_expected_income() {
let (_temp_dir, storage) = create_test_storage();
let service = IncomeService::new(&storage);
let period = BudgetPeriod::monthly(2025, 1);
service
.set_expected_income(&period, Money::from_cents(500000), None)
.unwrap();
let deleted = service.delete_expected_income(&period).unwrap();
assert!(deleted);
assert!(service.get_expected_income(&period).is_none());
let deleted_again = service.delete_expected_income(&period).unwrap();
assert!(!deleted_again);
}
#[test]
fn test_negative_amount_rejected() {
let (_temp_dir, storage) = create_test_storage();
let service = IncomeService::new(&storage);
let period = BudgetPeriod::monthly(2025, 1);
let result = service.set_expected_income(&period, Money::from_cents(-100), None);
assert!(result.is_err());
}
}