/*
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
//! This module contains implementations of dynamic step builders
//!
//! See [ReportingContext::register_dynamic_builder][super::types::ReportingContext::register_dynamic_builder].
use std::collections::HashMap;
use std::fmt::Display;
use async_trait::async_trait;
use tokio::sync::RwLock;
use crate::model::transaction::update_balances_from_transactions;
use super::calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies};
use super::executor::ReportingExecutionError;
use super::types::{
BalancesAt, BalancesBetween, DateArgs, DateStartDateEndArgs, ReportingContext,
ReportingProductId, ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs,
ReportingStepDynamicBuilder, ReportingStepId, Transactions,
};
/// Call [ReportingContext::register_dynamic_builder] for all dynamic builders provided by this module
pub fn register_dynamic_builders(context: &mut ReportingContext) {
GenerateBalances::register_dynamic_builder(context);
UpdateBalancesBetween::register_dynamic_builder(context);
UpdateBalancesAt::register_dynamic_builder(context);
// This is the least efficient way of generating BalancesBetween so put at the end
BalancesAtToBalancesBetween::register_dynamic_builder(context);
}
/// This dynamic builder automatically generates a [BalancesBetween] by subtracting [BalancesAt] between two dates
#[derive(Debug)]
pub struct BalancesAtToBalancesBetween {
step_name: String,
args: DateStartDateEndArgs,
}
impl BalancesAtToBalancesBetween {
// Implements BalancesAt, BalancesAt -> BalancesBetween
fn register_dynamic_builder(context: &mut ReportingContext) {
context.register_dynamic_builder(ReportingStepDynamicBuilder {
name: "BalancesAtToBalancesBetween",
can_build: Self::can_build,
build: Self::build,
});
}
fn can_build(
name: &str,
kind: ReportingProductKind,
args: &ReportingStepArgs,
steps: &Vec>,
dependencies: &ReportingGraphDependencies,
context: &ReportingContext,
) -> bool {
// Check for BalancesAt, BalancesAt -> BalancesBetween
if kind == ReportingProductKind::BalancesBetween {
if let ReportingStepArgs::DateStartDateEndArgs(args) = args {
match has_step_or_can_build(
&ReportingProductId {
name: name.to_string(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(DateArgs {
date: args.date_start.clone(),
}),
},
steps,
dependencies,
context,
) {
HasStepOrCanBuild::HasStep(_)
| HasStepOrCanBuild::CanLookup(_)
| HasStepOrCanBuild::CanBuild(_) => {
return true;
}
HasStepOrCanBuild::None => {}
}
} else {
return false;
}
}
return false;
}
fn build(
name: String,
_kind: ReportingProductKind,
args: ReportingStepArgs,
_steps: &Vec>,
_dependencies: &ReportingGraphDependencies,
_context: &ReportingContext,
) -> Box {
Box::new(BalancesAtToBalancesBetween {
step_name: name,
args: args.into(),
})
}
}
impl Display for BalancesAtToBalancesBetween {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"{} {{BalancesAtToBalancesBetween}}",
self.id()
))
}
}
#[async_trait]
impl ReportingStep for BalancesAtToBalancesBetween {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: self.step_name.clone(),
product_kinds: vec![ReportingProductKind::BalancesBetween],
args: ReportingStepArgs::DateStartDateEndArgs(self.args.clone()),
}
}
fn requires(&self, _context: &ReportingContext) -> Vec {
// BalancesAtToBalancesBetween depends on BalancesAt at both time points
vec![
ReportingProductId {
name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(DateArgs {
date: self.args.date_start.pred_opt().unwrap(), // Opening balance is the closing balance of the preceding day
}),
},
ReportingProductId {
name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(DateArgs {
date: self.args.date_end,
}),
},
]
}
async fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec>,
_dependencies: &ReportingGraphDependencies,
products: &RwLock,
) -> Result {
let products = products.read().await;
// Get balances at dates
let balances_start = &products
.get_or_err(&ReportingProductId {
name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(DateArgs {
date: self.args.date_start.pred_opt().unwrap(), // Opening balance is the closing balance of the preceding day
}),
})?
.downcast_ref::()
.unwrap()
.balances;
let balances_end = &products
.get_or_err(&ReportingProductId {
name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(DateArgs {
date: self.args.date_end,
}),
})?
.downcast_ref::()
.unwrap()
.balances;
// Compute balances_end - balances_start
let mut balances = BalancesBetween {
balances: balances_end.clone(),
};
for (account, balance) in balances_start.iter() {
let running_balance = balances.balances.get(account).unwrap_or(&0) - balance;
balances.balances.insert(account.clone(), running_balance);
}
// Store result
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: self.id().name,
kind: ReportingProductKind::BalancesBetween,
args: ReportingStepArgs::DateStartDateEndArgs(self.args.clone()),
},
Box::new(balances),
);
Ok(result)
}
}
/// This dynamic builder automatically generates a [BalancesAt] from a step which has no dependencies and generates [Transactions] (e.g. [PostUnreconciledStatementLines][super::steps::PostUnreconciledStatementLines])
#[derive(Debug)]
pub struct GenerateBalances {
step_name: String,
args: DateArgs,
}
impl GenerateBalances {
fn register_dynamic_builder(context: &mut ReportingContext) {
context.register_dynamic_builder(ReportingStepDynamicBuilder {
name: "GenerateBalances",
can_build: Self::can_build,
build: Self::build,
});
}
fn can_build(
name: &str,
kind: ReportingProductKind,
args: &ReportingStepArgs,
steps: &Vec>,
dependencies: &ReportingGraphDependencies,
context: &ReportingContext,
) -> bool {
// Check for Transactions -> BalancesAt
if kind == ReportingProductKind::BalancesAt {
// Try DateArgs
match has_step_or_can_build(
&ReportingProductId {
name: name.to_string(),
kind: ReportingProductKind::Transactions,
args: args.clone(),
},
steps,
dependencies,
context,
) {
HasStepOrCanBuild::HasStep(step) => {
// Check for () -> Transactions
if dependencies.dependencies_for_step(&step.id()).len() == 0 {
return true;
}
}
HasStepOrCanBuild::CanLookup(lookup_fn) => {
// Check for () -> Transactions
let step = lookup_fn(name, args.clone(), context);
if step.requires(context).len() == 0 {
return true;
}
}
HasStepOrCanBuild::CanBuild(_) | HasStepOrCanBuild::None => {}
}
// Try VoidArgs
match has_step_or_can_build(
&ReportingProductId {
name: name.to_string(),
kind: ReportingProductKind::Transactions,
args: ReportingStepArgs::VoidArgs,
},
steps,
dependencies,
context,
) {
HasStepOrCanBuild::HasStep(step) => {
// Check for () -> Transactions
if dependencies.dependencies_for_step(&step.id()).len() == 0 {
return true;
}
}
HasStepOrCanBuild::CanLookup(lookup_fn) => {
// Check for () -> Transactions
let step = lookup_fn(name, args.clone(), context);
if step.requires(context).len() == 0 {
return true;
}
}
HasStepOrCanBuild::CanBuild(_) | HasStepOrCanBuild::None => {}
}
}
return false;
}
fn build(
name: String,
_kind: ReportingProductKind,
args: ReportingStepArgs,
_steps: &Vec>,
_dependencies: &ReportingGraphDependencies,
_context: &ReportingContext,
) -> Box {
Box::new(GenerateBalances {
step_name: name,
args: args.into(),
})
}
}
impl Display for GenerateBalances {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{} {{GenerateBalances}}", self.id()))
}
}
#[async_trait]
impl ReportingStep for GenerateBalances {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: self.step_name.clone(),
product_kinds: vec![ReportingProductKind::BalancesAt],
args: ReportingStepArgs::DateArgs(self.args.clone()),
}
}
fn init_graph(
&self,
steps: &Vec>,
dependencies: &mut ReportingGraphDependencies,
context: &ReportingContext,
) {
// Add a dependency on the Transactions result
// Look up that step, so we can extract the appropriate args
// Try DateArgs
match has_step_or_can_build(
&ReportingProductId {
name: self.step_name.clone(),
kind: ReportingProductKind::Transactions,
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
steps,
dependencies,
context,
) {
HasStepOrCanBuild::HasStep(_)
| HasStepOrCanBuild::CanLookup(_)
| HasStepOrCanBuild::CanBuild(_) => {
dependencies.add_dependency(
self.id(),
ReportingProductId {
name: self.step_name.clone(),
kind: ReportingProductKind::Transactions,
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
);
return;
}
HasStepOrCanBuild::None => (),
}
// Must be VoidArgs (as checked in can_build)
dependencies.add_dependency(
self.id(),
ReportingProductId {
name: self.step_name.clone(),
kind: ReportingProductKind::Transactions,
args: ReportingStepArgs::VoidArgs,
},
);
}
async fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec>,
dependencies: &ReportingGraphDependencies,
products: &RwLock,
) -> Result {
let products = products.read().await;
// Get the transactions
let transactions_product = &dependencies.dependencies_for_step(&self.id())[0].product;
let transactions = &products
.get_or_err(transactions_product)?
.downcast_ref::()
.unwrap()
.transactions;
// Sum balances
let mut balances = BalancesAt {
balances: HashMap::new(),
};
update_balances_from_transactions(
&mut balances.balances,
transactions
.iter()
.filter(|t| t.transaction.dt.date() <= self.args.date),
);
// Store result
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
Box::new(balances),
);
Ok(result)
}
}
/// This dynamic builder automatically generates a [BalancesAt] from:
/// - a step which generates [Transactions] from [BalancesAt], or
/// - a step which generates [Transactions] from [BalancesBetween], and for which a [BalancesAt] is also available
#[derive(Debug)]
pub struct UpdateBalancesAt {
step_name: String,
args: DateArgs,
}
impl UpdateBalancesAt {
// Implements (BalancesAt -> Transactions) -> BalancesAt
fn register_dynamic_builder(context: &mut ReportingContext) {
context.register_dynamic_builder(ReportingStepDynamicBuilder {
name: "UpdateBalancesAt",
can_build: Self::can_build,
build: Self::build,
});
}
fn can_build(
name: &str,
kind: ReportingProductKind,
args: &ReportingStepArgs,
steps: &Vec>,
dependencies: &ReportingGraphDependencies,
context: &ReportingContext,
) -> bool {
if let ReportingStepArgs::DateArgs(args) = args {
// Check for Transactions -> BalancesAt
if kind == ReportingProductKind::BalancesAt {
// Initially no need to check args
if let Some(step) = steps.iter().find(|s| {
s.id().name == name
&& s.id()
.product_kinds
.contains(&ReportingProductKind::Transactions)
}) {
// Check for BalancesAt -> Transactions
let dependencies_for_step = dependencies.dependencies_for_step(&step.id());
if dependencies_for_step.len() == 1
&& dependencies_for_step[0].product.kind == ReportingProductKind::BalancesAt
{
return true;
}
// Check if BalancesBetween -> Transactions and BalancesAt is available
if dependencies_for_step.len() == 1
&& dependencies_for_step[0].product.kind
== ReportingProductKind::BalancesBetween
{
match has_step_or_can_build(
&ReportingProductId {
name: dependencies_for_step[0].product.name.clone(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(DateArgs { date: args.date }),
},
steps,
dependencies,
context,
) {
HasStepOrCanBuild::HasStep(_)
| HasStepOrCanBuild::CanLookup(_)
| HasStepOrCanBuild::CanBuild(_) => {
return true;
}
HasStepOrCanBuild::None => {}
}
}
}
}
return false;
} else {
return false;
}
}
fn build(
name: String,
_kind: ReportingProductKind,
args: ReportingStepArgs,
_steps: &Vec>,
_dependencies: &ReportingGraphDependencies,
_context: &ReportingContext,
) -> Box {
Box::new(UpdateBalancesAt {
step_name: name,
args: args.into(),
})
}
}
impl Display for UpdateBalancesAt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{} {{UpdateBalancesAt}}", self.id()))
}
}
#[async_trait]
impl ReportingStep for UpdateBalancesAt {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: self.step_name.clone(),
product_kinds: vec![ReportingProductKind::BalancesAt],
args: ReportingStepArgs::DateArgs(self.args.clone()),
}
}
fn init_graph(
&self,
steps: &Vec>,
dependencies: &mut ReportingGraphDependencies,
_context: &ReportingContext,
) {
// Add a dependency on the Transactions result
// Look up that step, so we can extract the appropriate args
let parent_step = steps
.iter()
.find(|s| {
s.id().name == self.step_name
&& s.id()
.product_kinds
.contains(&ReportingProductKind::Transactions)
})
.unwrap(); // Existence is checked in can_build
dependencies.add_dependency(
self.id(),
ReportingProductId {
name: self.step_name.clone(),
kind: ReportingProductKind::Transactions,
args: parent_step.id().args.clone(),
},
);
// Look up the BalancesAt step
let dependencies_for_step = dependencies.dependencies_for_step(&parent_step.id());
let dependency = &dependencies_for_step[0].product; // Existence and uniqueness checked in can_build
if dependency.kind == ReportingProductKind::BalancesAt {
// Directly depends on BalancesAt -> Transaction
// Do not need to add extra dependencies
} else {
// As checked in can_build, must depend on BalancesBetween -> Transaction with a BalancesAt available
dependencies.add_dependency(
self.id(),
ReportingProductId {
name: dependency.name.clone(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(DateArgs {
date: self.args.date,
}),
},
);
}
}
async fn execute(
&self,
_context: &ReportingContext,
steps: &Vec>,
dependencies: &ReportingGraphDependencies,
products: &RwLock,
) -> Result {
let products = products.read().await;
// Look up the parent step, so we can extract the appropriate args
let parent_step = steps
.iter()
.find(|s| {
s.id().name == self.step_name
&& s.id()
.product_kinds
.contains(&ReportingProductKind::Transactions)
})
.unwrap(); // Existence is checked in can_build
// Get transactions
let transactions = &products
.get_or_err(&ReportingProductId {
name: self.step_name.clone(),
kind: ReportingProductKind::Transactions,
args: parent_step.id().args,
})?
.downcast_ref::()
.unwrap()
.transactions;
// Look up the BalancesAt step
let dependencies_for_step = dependencies.dependencies_for_step(&parent_step.id());
let dependency = &dependencies_for_step[0].product; // Existence and uniqueness checked in can_build
let opening_balances_at;
if dependency.kind == ReportingProductKind::BalancesAt {
// Directly depends on BalancesAt -> Transaction
opening_balances_at = products
.get_or_err(&dependency)?
.downcast_ref::()
.unwrap();
} else {
// As checked in can_build, must depend on BalancesBetween -> Transaction with a BalancesAt available
opening_balances_at = products
.get_or_err(&ReportingProductId {
name: dependency.name.clone(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(DateArgs {
date: self.args.date,
}),
})?
.downcast_ref()
.unwrap();
}
// Sum balances
let mut balances = BalancesAt {
balances: opening_balances_at.balances.clone(),
};
update_balances_from_transactions(
&mut balances.balances,
transactions
.iter()
.filter(|t| t.transaction.dt.date() <= self.args.date),
);
// Store result
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
Box::new(balances),
);
Ok(result)
}
}
/// This dynamic builder automatically generates a [BalancesBetween] from a step which generates [Transactions] from [BalancesBetween]
#[derive(Debug)]
pub struct UpdateBalancesBetween {
step_name: String,
args: DateStartDateEndArgs,
}
impl UpdateBalancesBetween {
fn register_dynamic_builder(context: &mut ReportingContext) {
context.register_dynamic_builder(ReportingStepDynamicBuilder {
name: "UpdateBalancesBetween",
can_build: Self::can_build,
build: Self::build,
});
}
fn can_build(
name: &str,
kind: ReportingProductKind,
_args: &ReportingStepArgs,
steps: &Vec>,
dependencies: &ReportingGraphDependencies,
_context: &ReportingContext,
) -> bool {
// Check for Transactions -> BalancesBetween
if kind == ReportingProductKind::BalancesBetween {
// Initially no need to check args
if let Some(step) = steps.iter().find(|s| {
s.id().name == name
&& s.id()
.product_kinds
.contains(&ReportingProductKind::Transactions)
}) {
// Check for BalancesBetween -> Transactions
let dependencies_for_step = dependencies.dependencies_for_step(&step.id());
if dependencies_for_step.len() == 1
&& dependencies_for_step[0].product.kind
== ReportingProductKind::BalancesBetween
{
return true;
}
}
}
return false;
}
fn build(
name: String,
_kind: ReportingProductKind,
args: ReportingStepArgs,
_steps: &Vec>,
_dependencies: &ReportingGraphDependencies,
_context: &ReportingContext,
) -> Box {
Box::new(UpdateBalancesBetween {
step_name: name,
args: args.into(),
})
}
}
impl Display for UpdateBalancesBetween {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{} {{UpdateBalancesBetween}}", self.id()))
}
}
#[async_trait]
impl ReportingStep for UpdateBalancesBetween {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: self.step_name.clone(),
product_kinds: vec![ReportingProductKind::BalancesBetween],
args: ReportingStepArgs::DateStartDateEndArgs(self.args.clone()),
}
}
fn init_graph(
&self,
steps: &Vec>,
dependencies: &mut ReportingGraphDependencies,
_context: &ReportingContext,
) {
// Add a dependency on the Transactions result
// Look up that step, so we can extract the appropriate args
let parent_step = steps
.iter()
.find(|s| {
s.id().name == self.step_name
&& s.id()
.product_kinds
.contains(&ReportingProductKind::Transactions)
})
.unwrap(); // Existence is checked in can_build
dependencies.add_dependency(
self.id(),
ReportingProductId {
name: self.step_name.clone(),
kind: ReportingProductKind::Transactions,
args: parent_step.id().args,
},
);
// Look up the BalancesBetween step
let dependencies_for_step = dependencies.dependencies_for_step(&parent_step.id());
let balances_between_product = &dependencies_for_step[0].product; // Existence and uniqueness checked in can_build
if matches!(
balances_between_product.args,
ReportingStepArgs::DateStartDateEndArgs(_)
) {
// Directly depends on BalanceBetween -> Transaction with appropriate date
// Do not need to add extra dependencies
} else {
// Depends on BalanceBetween with appropriate date
dependencies.add_dependency(
self.id(),
ReportingProductId {
name: balances_between_product.name.clone(),
kind: ReportingProductKind::BalancesBetween,
args: ReportingStepArgs::DateStartDateEndArgs(self.args.clone()),
},
);
}
}
async fn execute(
&self,
_context: &ReportingContext,
steps: &Vec>,
dependencies: &ReportingGraphDependencies,
products: &RwLock,
) -> Result {
let products = products.read().await;
// Look up the parent step, so we can extract the appropriate args
let parent_step = steps
.iter()
.find(|s| {
s.id().name == self.step_name
&& s.id()
.product_kinds
.contains(&ReportingProductKind::Transactions)
})
.unwrap(); // Existence is checked in can_build
// Get transactions
let transactions = &products
.get_or_err(&ReportingProductId {
name: self.step_name.clone(),
kind: ReportingProductKind::Transactions,
args: parent_step.id().args,
})?
.downcast_ref::()
.unwrap()
.transactions;
// Look up the BalancesBetween step
let dependencies_for_step = dependencies.dependencies_for_step(&parent_step.id());
let balances_between_product = &dependencies_for_step[0].product; // Existence and uniqueness is checked in can_build
// Get opening balances
let opening_balances = &products
.get_or_err(&ReportingProductId {
name: balances_between_product.name.clone(),
kind: ReportingProductKind::BalancesBetween,
args: ReportingStepArgs::DateStartDateEndArgs(self.args.clone()),
})?
.downcast_ref::()
.unwrap()
.balances;
// Sum balances
let mut balances = BalancesBetween {
balances: opening_balances.clone(),
};
update_balances_from_transactions(
&mut balances.balances,
transactions.iter().filter(|t| {
t.transaction.dt.date() >= self.args.date_start
&& t.transaction.dt.date() <= self.args.date_end
}),
);
// Store result
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: self.step_name.clone(),
kind: ReportingProductKind::BalancesBetween,
args: ReportingStepArgs::DateStartDateEndArgs(self.args.clone()),
},
Box::new(balances),
);
Ok(result)
}
}