use bevy::prelude::*;
use super::components::*;
use super::events::*;
#[derive(Resource, Reflect, Clone)]
#[reflect(Resource)]
pub struct AccountingConfig {
pub settlement_period_days: u32,
}
impl Default for AccountingConfig {
fn default() -> Self {
Self {
settlement_period_days: 7, }
}
}
#[derive(Resource, Default, Reflect)]
#[reflect(Resource)]
pub struct GameDay {
pub current: u32,
}
pub fn handle_day_changed(
mut commands: Commands,
config: Res<AccountingConfig>,
day: Res<GameDay>,
mut orgs: Query<(Entity, &mut SettlementHistory), With<BudgetLedger>>,
) {
let current_day = day.current;
for (org_entity, history) in orgs.iter_mut() {
if history.should_run_settlement(current_day, config.settlement_period_days) {
let period = current_day / config.settlement_period_days;
commands.write_message(SettlementRequested {
organization: org_entity,
period,
day: current_day,
});
}
}
}
pub fn start_settlement_sessions(
mut commands: Commands,
mut events: MessageReader<SettlementRequested>,
) {
for event in events.read() {
commands.spawn((
SettlementSession {
organization: event.organization,
period: event.period,
day: event.day,
status: SettlementStatus::Calculating,
},
SettlementCalculation::default(),
));
}
}
pub fn calculate_income(
mut sessions: Query<
(Entity, &SettlementSession, &mut SettlementCalculation),
With<SettlementSession>,
>,
mut commands: Commands,
) {
for (session_entity, session, mut calc) in sessions.iter_mut() {
if session.status != SettlementStatus::Calculating {
continue;
}
commands.trigger(IncomeCalculationEvent {
settlement_entity: session_entity,
organization: session.organization,
period: session.period,
sources: calc.income_sources.clone(),
});
calc.total_income = calc.income_sources.iter().map(|s| s.amount).sum();
}
}
pub fn calculate_expenses(
mut sessions: Query<
(Entity, &SettlementSession, &mut SettlementCalculation),
With<SettlementSession>,
>,
mut commands: Commands,
) {
for (session_entity, session, mut calc) in sessions.iter_mut() {
if session.status != SettlementStatus::Calculating {
continue;
}
commands.trigger(ExpenseCalculationEvent {
settlement_entity: session_entity,
organization: session.organization,
period: session.period,
items: calc.expense_items.clone(),
});
calc.total_expenses = calc.expense_items.iter().map(|e| e.amount).sum();
}
}
pub fn finalize_settlements(
mut commands: Commands,
mut sessions: Query<(Entity, &mut SettlementSession, &SettlementCalculation)>,
mut ledgers: Query<&mut BudgetLedger>,
mut histories: Query<&mut SettlementHistory>,
) {
for (session_entity, mut session, calc) in sessions.iter_mut() {
if session.status != SettlementStatus::Calculating {
continue;
}
let net = calc.total_income - calc.total_expenses;
if let Ok(mut ledger) = ledgers.get_mut(session.organization) {
ledger.cash = ledger.cash.saturating_add(net);
if let Ok(mut history) = histories.get_mut(session.organization) {
history.record_settlement(SettlementRecord {
period: session.period,
day: session.day,
income: calc.total_income,
expenses: calc.total_expenses,
net,
cash_after: ledger.cash,
});
}
commands.write_message(SettlementCompletedEvent {
organization: session.organization,
period: session.period,
income: calc.total_income,
expenses: calc.total_expenses,
net,
});
session.status = SettlementStatus::Completed;
} else {
warn!(
"Settlement session {:?} references deleted or invalid organization {:?}",
session_entity, session.organization
);
commands.entity(session_entity).despawn();
}
}
}
pub fn handle_budget_transfers(
mut commands: Commands,
mut events: MessageReader<BudgetTransferRequested>,
mut ledgers: Query<&mut BudgetLedger>,
) {
for event in events.read() {
if let Ok(mut ledger) = ledgers.get_mut(event.organization) {
if ledger.transfer(event.from, event.to, event.amount) {
commands.write_message(BudgetTransferredEvent {
organization: event.organization,
from: event.from,
to: event.to,
amount: event.amount,
});
} else {
warn!(
"Budget transfer failed: insufficient funds in {:?} channel",
event.from
);
}
} else {
warn!(
"Budget transfer requested for deleted or invalid organization {:?}",
event.organization
);
}
}
}
pub fn cleanup_settlement_sessions(
mut commands: Commands,
sessions: Query<(Entity, &SettlementSession)>,
) {
for (entity, session) in sessions.iter() {
if session.status == SettlementStatus::Completed {
commands.entity(entity).despawn();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_accounting_config_default() {
let config = AccountingConfig::default();
assert_eq!(config.settlement_period_days, 7);
}
#[test]
fn test_settlement_flow() {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.insert_resource(AccountingConfig::default());
app.insert_resource(GameDay { current: 7 });
app.add_message::<SettlementRequested>();
app.add_message::<SettlementCompletedEvent>();
app.add_systems(
Update,
(
start_settlement_sessions,
calculate_income,
calculate_expenses,
finalize_settlements,
)
.chain(),
);
let org = app
.world_mut()
.spawn((
Organization {
name: "Test Corp".to_string(),
},
BudgetLedger::default(),
SettlementHistory::default(),
))
.id();
app.world_mut().write_message(SettlementRequested {
organization: org,
period: 1,
day: 7,
});
app.update();
let sessions: Vec<_> = app
.world_mut()
.query::<&SettlementSession>()
.iter(app.world())
.collect();
assert_eq!(sessions.len(), 1);
assert_eq!(sessions[0].period, 1);
}
#[test]
fn test_budget_transfer_system() {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.add_message::<BudgetTransferRequested>();
app.add_message::<BudgetTransferredEvent>();
app.add_systems(Update, handle_budget_transfers);
let org = app
.world_mut()
.spawn((
Organization {
name: "Test Corp".to_string(),
},
BudgetLedger::default(),
))
.id();
app.world_mut().write_message(BudgetTransferRequested {
organization: org,
from: BudgetChannel::Cash,
to: BudgetChannel::Research,
amount: Currency::new(100),
});
app.update();
let ledger = app.world().get::<BudgetLedger>(org).unwrap();
assert_eq!(ledger.cash, Currency::new(2300));
assert_eq!(ledger.research_pool, Currency::new(700));
}
#[test]
fn test_deleted_organization_handling() {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.add_systems(Update, finalize_settlements);
let org = app
.world_mut()
.spawn((
Organization {
name: "Test Corp".to_string(),
},
BudgetLedger::default(),
))
.id();
app.world_mut().entity_mut(org).despawn();
app.world_mut().spawn((
SettlementSession {
organization: org,
period: 1,
day: 7,
status: SettlementStatus::Calculating,
},
SettlementCalculation::default(),
));
app.update();
let sessions: Vec<_> = app
.world_mut()
.query::<&SettlementSession>()
.iter(app.world())
.collect();
assert_eq!(sessions.len(), 0);
}
}