use bevy::prelude::*;
use std::ops::{Add, AddAssign, Sub, SubAssign};
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Reflect)]
#[reflect(opaque)]
pub struct Currency(i64);
impl Currency {
pub const ZERO: Currency = Currency(0);
pub fn new(amount: i64) -> Self {
Self(amount)
}
pub fn amount(&self) -> i64 {
self.0
}
pub fn saturating_add(self, other: Currency) -> Currency {
Currency(self.0.saturating_add(other.0))
}
pub fn saturating_sub(self, other: Currency) -> Currency {
Currency(self.0.saturating_sub(other.0))
}
pub fn is_positive(&self) -> bool {
self.0 > 0
}
pub fn is_zero(&self) -> bool {
self.0 == 0
}
pub fn is_negative(&self) -> bool {
self.0 < 0
}
pub fn abs(&self) -> Currency {
Currency(self.0.abs())
}
}
impl Add for Currency {
type Output = Currency;
fn add(self, other: Currency) -> Currency {
self.saturating_add(other)
}
}
impl AddAssign for Currency {
fn add_assign(&mut self, other: Currency) {
*self = self.saturating_add(other);
}
}
impl Sub for Currency {
type Output = Currency;
fn sub(self, other: Currency) -> Currency {
self.saturating_sub(other)
}
}
impl SubAssign for Currency {
fn sub_assign(&mut self, other: Currency) {
*self = self.saturating_sub(other);
}
}
impl std::iter::Sum for Currency {
fn sum<I: Iterator<Item = Currency>>(iter: I) -> Self {
iter.fold(Currency::ZERO, |acc, x| acc + x)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)]
pub enum BudgetChannel {
Cash,
Research,
Operations,
Reserve,
Innovation,
Security,
}
#[derive(Component, Reflect, Clone)]
#[reflect(Component)]
pub struct BudgetLedger {
pub cash: Currency,
pub research_pool: Currency,
pub ops_pool: Currency,
pub reserve: Currency,
pub innovation_fund: Currency,
pub security_fund: Currency,
}
impl Default for BudgetLedger {
fn default() -> Self {
Self {
cash: Currency::new(2400),
research_pool: Currency::new(600),
ops_pool: Currency::new(600),
reserve: Currency::new(400),
innovation_fund: Currency::ZERO,
security_fund: Currency::ZERO,
}
}
}
impl BudgetLedger {
pub fn with_starting_cash(cash: Currency) -> Self {
Self {
cash,
..Default::default()
}
}
pub fn can_spend(&self, channel: BudgetChannel, amount: Currency) -> bool {
self.get_channel(channel) >= &amount
}
pub fn transfer(&mut self, from: BudgetChannel, to: BudgetChannel, amount: Currency) -> bool {
if !self.can_spend(from, amount) {
return false;
}
*self.get_channel_mut(from) -= amount;
*self.get_channel_mut(to) += amount;
true
}
pub fn try_spend(&mut self, channel: BudgetChannel, amount: Currency) -> bool {
if !self.can_spend(channel, amount) {
return false;
}
*self.get_channel_mut(channel) -= amount;
true
}
pub fn total_liquid(&self) -> Currency {
self.cash + self.research_pool + self.ops_pool + self.reserve
}
pub fn total_assets(&self) -> Currency {
self.total_liquid() + self.innovation_fund + self.security_fund
}
fn get_channel(&self, channel: BudgetChannel) -> &Currency {
match channel {
BudgetChannel::Cash => &self.cash,
BudgetChannel::Research => &self.research_pool,
BudgetChannel::Operations => &self.ops_pool,
BudgetChannel::Reserve => &self.reserve,
BudgetChannel::Innovation => &self.innovation_fund,
BudgetChannel::Security => &self.security_fund,
}
}
fn get_channel_mut(&mut self, channel: BudgetChannel) -> &mut Currency {
match channel {
BudgetChannel::Cash => &mut self.cash,
BudgetChannel::Research => &mut self.research_pool,
BudgetChannel::Operations => &mut self.ops_pool,
BudgetChannel::Reserve => &mut self.reserve,
BudgetChannel::Innovation => &mut self.innovation_fund,
BudgetChannel::Security => &mut self.security_fund,
}
}
}
#[derive(Component, Reflect, Clone)]
#[reflect(Component)]
#[derive(Default)]
pub struct SettlementHistory {
pub last_settlement_period: u32,
pub records: Vec<SettlementRecord>,
}
impl SettlementHistory {
pub fn should_run_settlement(&self, current_day: u32, period_days: u32) -> bool {
if period_days == 0 {
return false;
}
let expected_period = current_day / period_days;
expected_period > self.last_settlement_period
}
pub fn record_settlement(&mut self, record: SettlementRecord) {
self.last_settlement_period = record.period;
self.records.push(record);
if self.records.len() > 20 {
self.records.remove(0);
}
}
}
#[derive(Clone, Reflect)]
pub struct SettlementRecord {
pub period: u32,
pub day: u32,
pub income: Currency,
pub expenses: Currency,
pub net: Currency,
pub cash_after: Currency,
}
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct SettlementSession {
pub organization: Entity,
pub period: u32,
pub day: u32,
pub status: SettlementStatus,
}
#[derive(Reflect, Clone, Copy, PartialEq, Eq)]
pub enum SettlementStatus {
Calculating,
Completed,
}
#[derive(Component, Reflect, Clone, Default)]
#[reflect(Component)]
pub struct SettlementCalculation {
pub income_sources: Vec<IncomeSource>,
pub expense_items: Vec<ExpenseItem>,
pub total_income: Currency,
pub total_expenses: Currency,
pub net: Currency,
}
#[derive(Reflect, Clone)]
pub struct IncomeSource {
pub category: String,
pub amount: Currency,
}
#[derive(Reflect, Clone)]
pub struct ExpenseItem {
pub category: String,
pub amount: Currency,
}
#[derive(Component, Reflect, Clone)]
#[reflect(Component)]
pub struct UniqueId(pub String);
#[derive(Component, Reflect, Clone)]
#[reflect(Component)]
pub struct Organization {
pub name: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_currency_arithmetic() {
let a = Currency::new(100);
let b = Currency::new(50);
assert_eq!((a + b).amount(), 150);
assert_eq!((a - b).amount(), 50);
}
#[test]
fn test_currency_saturation() {
let max = Currency::new(i64::MAX);
let one = Currency::new(1);
let result = max.saturating_add(one);
assert_eq!(result.amount(), i64::MAX);
let min = Currency::new(i64::MIN);
let result = min.saturating_sub(one);
assert_eq!(result.amount(), i64::MIN); }
#[test]
fn test_budget_ledger_transfer() {
let mut ledger = BudgetLedger::default();
assert!(ledger.transfer(
BudgetChannel::Cash,
BudgetChannel::Research,
Currency::new(100)
));
assert_eq!(ledger.cash, Currency::new(2300));
assert_eq!(ledger.research_pool, Currency::new(700));
}
#[test]
fn test_budget_ledger_transfer_insufficient() {
let mut ledger = BudgetLedger::default();
assert!(!ledger.transfer(
BudgetChannel::Cash,
BudgetChannel::Research,
Currency::new(10000)
));
assert_eq!(ledger.cash, Currency::new(2400));
assert_eq!(ledger.research_pool, Currency::new(600));
}
#[test]
fn test_settlement_history_should_run() {
let history = SettlementHistory::default();
assert!(history.should_run_settlement(7, 7));
let history = SettlementHistory {
last_settlement_period: 1,
records: Vec::new(),
};
assert!(!history.should_run_settlement(7, 7));
assert!(history.should_run_settlement(14, 7));
}
#[test]
fn test_budget_totals() {
let ledger = BudgetLedger::default();
let liquid = ledger.total_liquid();
assert_eq!(liquid, Currency::new(2400 + 600 + 600 + 400));
let total = ledger.total_assets();
assert_eq!(total, liquid); }
}