kcode-k1-accounting 0.1.0

Thread-safe in-memory accounting ledger for exact decimal usage totals.
Documentation
use std::{
    collections::BTreeMap,
    sync::{Arc, Mutex, MutexGuard},
};

use rust_decimal::Decimal;

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct UsageValue {
    pub units: Decimal,
    pub price_cents: Decimal,
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct AccountingEvent {
    pub source: String,
    pub operation: String,
    pub usage: BTreeMap<String, UsageValue>,
}

#[derive(Clone, Default)]
pub struct Accounting {
    ledger: Arc<Mutex<Ledger>>,
}

#[derive(Default)]
struct Ledger {
    events: Vec<Arc<AccountingEvent>>,
}

impl Accounting {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn record(&self, event: &AccountingEvent) {
        let event = Arc::new(event.clone());
        self.lock().events.push(event);
    }

    pub fn entries(&self) -> Vec<AccountingEvent> {
        let events = self.lock().events.clone();
        events.into_iter().map(|event| (*event).clone()).collect()
    }

    pub fn totals(&self) -> BTreeMap<String, UsageValue> {
        let events = self.lock().events.clone();
        let mut totals: BTreeMap<String, UsageValue> = BTreeMap::new();

        for event in events {
            for (key, value) in &event.usage {
                let total = totals.entry(key.clone()).or_default();
                total.units += value.units;
                total.price_cents += value.price_cents;
            }
        }

        totals
    }

    fn lock(&self) -> MutexGuard<'_, Ledger> {
        self.ledger
            .lock()
            .unwrap_or_else(|poisoned| poisoned.into_inner())
    }
}