Skip to contents
library(tidyverse)
#> ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
#>  dplyr     1.1.4      readr     2.1.5
#>  forcats   1.0.0      stringr   1.5.1
#>  ggplot2   3.5.2      tibble    3.3.0
#>  lubridate 1.9.4      tidyr     1.3.1
#>  purrr     1.1.0     
#> ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
#>  dplyr::filter() masks stats::filter()
#>  dplyr::lag()    masks stats::lag()
#>  Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(finman)

📦 What is finman?

The finman package powers the logic of the Finance Manager system. It defines core account types (Main, Child, Grandchild), allocation rules, transaction tracking, and file-based persistence.

🧠 Key Concepts

Account Hierarchy

  • MainAccount: Receives income and distributes it.
  • ChildAccount: Receives allocations and may hold rules.
  • GrandchildAccount: Tracks obligations (e.g., rent, school fees).

Account Behavior

  • Accounts deactivate when fully funded.
  • They reactivate based on periodic due dates.
  • Loan accounts can pull funds automatically from the main account.

Persistence

🚀 Creating an Account Tree

main <- MainAccount$new("Main")
needs <- ChildAccount$new("Needs", allocation = 0.5)
savings <- ChildAccount$new("Savings", allocation = 0.3)
debt <- ChildAccount$new("Debt", allocation = 0.2)

main$add_child_account(needs)
main$add_child_account(savings)
main$add_child_account(debt)

rent <- GrandchildAccount$new(
  "Rent", freq = 30, due_date = today() + 5, fixed_amount = 5000
)
needs$add_child_account(rent)

💸 Allocation and Transactions

Accounts can be allocated funds in several ways:

  • Percentage Allocation
    Child and grandchild accounts receive money from their parent based on predefined percentages. Depositing into a parent triggers automatic distribution. Parent balances remain zero unless children are fully funded.

  • Direct Deposits
    Depositing directly into a lower-level account is treated as allocation to that account.

  • Refunds
    If a child account receives more than it needs, the extra amount is refunded to the parent. The parent redistributes this unless all child accounts are inactive.

Example:

# Deposit income (in other words allocate money to main account)
main$deposit(20000, channel = "ABSA")
#> Withdrew: 10000 via Allocation to Needs - Transaction ID: sys2
#> No active child accounts available.
#> Deposited: 10000 via Allocation from Main - Transaction ID: sys1 
#> Withdrew: 6000 via Allocation to Savings - Transaction ID: sys3 
#> Deposited: 6000 via Allocation from Main - Transaction ID: sys1 
#> Withdrew: 4000 via Allocation to Debt - Transaction ID: sys4 
#> Deposited: 4000 via Allocation from Main - Transaction ID: sys1 
#> Deposited: 20000 via ABSA - Transaction ID: sys1

📊 Viewing Allocation Results

This is how money trickled down.

# balance
main$get_balance()              # balance in main account
#> Current Balance: 0
#> [1] 0
needs$get_balance()             # balance in needs account
#> Current Balance: 10000
#> [1] 10000
savings$get_balance()           # balance in savings account
#> Current Balance: 6000
#> [1] 6000
debt$get_balance()              # balance in debt account
#> Current Balance: 4000
#> [1] 4000
rent$get_balance()              # balance in rent account
#> Current Balance: 0
#> [1] 0

# Total balance
main$compute_total_balance()    # total balance in main account+its children
#> [1] 20000
                                # this is literary the accumulated money in your
                                # bank account

needs$compute_total_balance()   # total balance in needs account+ its children
#> [1] 10000
                                # what amount in your bank account is there to 
                                # cover needs

savings$compute_total_balance()   # total balance in savings account+ its children
#> [1] 6000
                                  # what amount in your bank account is there to 
                                  # cover savings

debt$compute_total_balance()   # total balance in debt account+ its children
#> [1] 4000
                                  # what amount in your bank account is there to 
                                  # cover debt

rent$compute_total_balance()   # total balance in rent(it has no children)
#> [1] 0
                               # what amount in your bank account is there to 
                               # cover rent (a sinking fund)
                            

# amount due 
main$compute_total_due()        # Total amount of debt(current+longterm)
#> [1] 5000

needs$compute_total_due()       # Total amount of debt from needs (current+longterm)
#> [1] 5000

savings$compute_total_due()     # Total amount of debt from savings (current+longterm)
#> [1] 0
                                # thes are unmet fixed savings etc.

rent$compute_total_due()     # Total amount of debt from rent (current+longterm)
#> [1] 5000
                             # this is what you need to pay from the sinking fund
                             # for rent

❓ Why is Rent’s balance zero?

rent$status
#> [1] "inactive"

Answer: The Rent account is inactive.

When it was created, we didn’t assign it an allocation percentage, so it defaulted to zero. Accounts with zero allocation are considered inactive — this tells the system not to fund them.

Let’s fix that:

needs$set_child_allocation("Rent", 0.5)
#> 
#> Child Accounts of Needs :
#> - Rent
rent$change_status("active")
#> Rent has become active .

💸 Direct Deposits to Grandchild Accounts

You can also allocate funds by depositing directly into a grandchild account.

rent$deposit(20000, channel = "Barclays")
#> Deposited: 5000 via Barclays - Transaction ID: sys1 
#> Rent has become inactive .
#> Extra amount of 15000 moved to Needs 
#> Rent fully funded for 1 period(s)

Since Rent is now fully funded, it becomes inactive and stops drawing from Needs. If there were other active siblings, they would receive future allocations. Over time, this behavior self-corrects over- or under-funding.

rent$get_account_status()
#> Rent is inactive
#> [1] "inactive"
# balance
rent$get_balance()    # get rent balance
#> Current Balance: 5000
#> [1] 5000
needs$get_balance()   # needs(parent) balance has increased by the refund amount
#> Current Balance: 25000
#> [1] 25000

# overall balance
needs$compute_total_balance()  # need account has more money
#> [1] 30000
needs$compute_total_due()  # need total due has decreased
#> [1] 0
main$compute_total_due()   # even on the whole tree there is decrease
#> [1] 0
                           # you have reduced total payable.  

📊 Advanced Metrics

Spending tracks actual payments (withdrawals).
Allocated Amount is the total amount directed to an account and its descendants.
Income Utilization shows how much of that allocation has been spent.

If utilization is 0%, it means you’ve saved for obligations but haven’t actually spent the money.

main$allocated_amount() # total income allocated
#> [1] 40000
main$spending()         # total spend
#> [1] 0
main$income_utilization() # utilization of allocated amount?
#> [1] 0

needs$allocated_amount() # total income allocated to needs
#> [1] 30000
needs$spending()         # how much of the allocation is spend
#> [1] 0
needs$income_utilization() # utilization level
#> [1] 0


savings$allocated_amount() # total income allocated to savings
#> [1] 6000
savings$spending()         # how much of the allocation is spend
#> [1] 0
savings$income_utilization() # utilization level
#> [1] 0

debt$allocated_amount() # total income allocated to debt
#> [1] 4000
debt$spending()         # how much of the allocation is spend
#> [1] 0
debt$income_utilization() # utilization level
#> [1] 0

🧾 Spending Some Amount

Let’s simulate spending:

rent$withdraw(5000,channel="ABSA")      # paid rent
#> Withdrew: 5000 via ABSA - Transaction ID: sys2
savings$withdraw(6000,channel="ABSA")   # withdraw to pay savings the specifics
#> Withdrew: 6000 via ABSA - Transaction ID: sys1
                                        # Not known we can specify this by attaching
                                        # which are the specific savings

debt$withdraw(4000,channel="ABSA")      # withdraw to pay savings the specifics
#> Withdrew: 4000 via ABSA - Transaction ID: sys1

📉 After Spending: Utilization Analysis

main$allocated_amount() # total income allocated
#> [1] 40000
main$spending()         # total spend
#> [1] 15000
main$income_utilization() # utilization of allocated amount?
#> [1] 0.375

needs$allocated_amount() # total income allocated to needs
#> [1] 30000
needs$spending()         # how much of the allocation is spend
#> [1] 5000
needs$income_utilization() # utilization level
#> [1] 0.1666667


savings$allocated_amount() # total income allocated to savings
#> [1] 6000
savings$spending()         # how much of the allocation is spend
#> [1] 6000
savings$income_utilization() # utilization level
#> [1] 1

debt$allocated_amount() # total income allocated to debt
#> [1] 4000
debt$spending()         # how much of the allocation is spend
#> [1] 4000
debt$income_utilization() # utilization level
#> [1] 1

Example interpretation:
A 37.5% utilization rate means only 37.5% of allocated funds were actually used. The rest remains idle.
In Needs, only Rent was defined — hence low utilization. You may want to define more obligations to reflect your actual needs.

🔄 Advanced Usage

  • Set obligation frequency: set_account_freq()
  • Define fixed obligations: set_fixed_amount()
  • Configure time periods: set_account_periods()
  • Use with_account_lock() for concurrency-safe operations

📚 Next Steps

  • View API endpoints via the Plumber API docs
  • Try the full interface in the Shiny App

💖 Sponsors

Support my work through GitHub Sponsors!

GitHub Sponsors