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)

🔄 Account Lifecycle and Behavior

This vignette explains how finman manages the life cycle of accounts — from creation, activation, allocation, and reactivation after obligations are met or missed.

🏗️ 1. Account Creation

Accounts in finman are hierarchical:

  • MainAccount sits at the top and receives deposits.
  • ChildAccounts receive allocations from the main account (group of obligations).
  • GrandchildAccounts track specific obligations like rent, loans insurance etc.
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)
# Simulate overdue
rent <- GrandchildAccount$new("Rent", 
                              freq =1,
                              due_date = Sys.Date() - 6,
                              fixed_amount = 5000,
                              account_type = "Bill")
needs$add_child_account(rent)

🔋 2. Account Status: Active vs Inactive

  • Accounts with no allocation (allocation = 0) are inactive by default.
  • Accounts become inactive when their obligations are fully met.
  • Inactive accounts do not receive allocations.
rent$status  # "inactive"
#> [1] "inactive"

🛠️ Reactivating Accounts

Accounts are reactivated automatically when:

  • Their due date is reached or passed.
  • They have pending obligations (unpaid periods).

You can also manually reactivate:

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

🔁 3. Automatic Reactivation by Due Date

When a deposit is made for bills and the due date has passed, the account will automatically extend the due period and reactivate itself:

rent$deposit(10000, channel = "Bank",date = Sys.Date() - 8) # funded days before due
#> Deposited: 5000 via Bank - Transaction ID: sys1 
#> Rent has become inactive .
#> Extra amount of 5000 moved to Needs 
#> Rent fully funded for 1 period(s)
rent$get_account_status()
#> Rent is inactive
#> [1] "inactive"
rent$get_account_periods()
#> [1] 1
rent$deposit(1000, channel = "Bank",date = Sys.Date() - 3)
#> Due date extended. Number of periods unpaid: 2 
#> Rent reactivated. Outstanding balance due: 5000 
#> Deposited: 1000 via Bank - Transaction ID: sys2
rent$get_account_status()
#> Rent is active
#> [1] "active"
rent$get_account_periods()
#> [1] 2

📈 4. Priority-Based Greedy Allocation

If multiple children are active, allocations follow the priority field:

  • Higher priority accounts receive funds first.
  • Allocation is based on defined percentages (allocation).so all active children receive their share proportionally.
rent$change_status("active")  # ensure rent is active
#> Rent has become active .
main$deposit(20000, channel = "Equity Bank")
#> Withdrew: 10000 via Allocation to Needs - Transaction ID: sys2 
#> Withdrew: 10000 via Allocation to Rent - Transaction ID: sys2 
#> Due date extended. Number of periods unpaid: 3 
#> Rent reactivated. Outstanding balance due: 9000 
#> Deposited: 9000 via Allocation from Needs - Transaction ID: sys1 
#> Rent has become inactive .
#> Extra amount of 1000 moved to Needs 
#> Rent fully funded for 3 period(s)
#> 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 Equity Bank - Transaction ID: sys1

rent$get_balance()      # Rent gets funds
#> Current Balance: 15000
#> [1] 15000
needs$get_balance()     # Needs balance reflects leftover after rent
#> Current Balance: 6000
#> [1] 6000
main$get_balance()      # Should be zero if all children are active
#> Current Balance: 0
#> [1] 0

However, since funds are distributed proportionally, the effect of priority can be subtle — all active accounts still receive their share.

Priority becomes most apparent when the deposited amount is too small to distribute among all children. In such cases, the full amount is allocated to the child with the highest priority.

# set child priorities

needs$set_priority(3)
#> Priority for Needs set to 3
savings$set_priority(2)
#> Priority for Savings set to 2
debt$set_priority(1)
#> Priority for Debt set to 1

# deposit money
main$deposit(10000, channel = "Equity Bank",transaction_number = "Trans1")
#> Withdrew: 5000 via Allocation to Needs - Transaction ID: sys5
#> No active child accounts available.
#> Deposited: 5000 via Allocation from Main - Transaction ID: Trans1 
#> Withdrew: 3000 via Allocation to Savings - Transaction ID: sys6 
#> Deposited: 3000 via Allocation from Main - Transaction ID: Trans1 
#> Withdrew: 2000 via Allocation to Debt - Transaction ID: sys7 
#> Deposited: 2000 via Allocation from Main - Transaction ID: Trans1 
#> Deposited: 10000 via Equity Bank - Transaction ID: Trans1

# distributions order follows priority see timestamps

needstime=needs$transactions%>%filter(TransactionID=="Trans1")
savingstime=savings$transactions%>%filter(TransactionID=="Trans1")
debttime=debt$transactions%>%filter(TransactionID=="Trans1")
sprintf("needs: %s",needstime$Date)
#> [1] "needs: 2025-08-01 19:23:26.177186"
sprintf("savings: %s",savingstime$Date)
#> [1] "savings: 2025-08-01 19:23:26.17944"
sprintf("debt: %s",debttime$Date)
#> [1] "debt: 2025-08-01 19:23:26.181154"

💡 If an account receives more than it needs, the extra is refunded to the parent and possibly redistributed.

🔄 Handling Tiny Refunds and Preventing Cyclic Redistribution

Since refunds are returned to the parent account, they are treated as regular deposits. This ensures that all deposit checks and validations apply uniformly. However, because deposits trigger redistribution to child accounts, this can inadvertently lead to cyclic loops, especially when the refunded amount is very small (e.g., 0.00001).

To prevent such infinite allocation loops, any amount less than 0.10 is not redistributed among all children. Instead, the system deposits the entire small amount into the highest-priority active child account. This strategy maintains consistency while avoiding unnecessary micro-redistribution.

main$deposit(0.09, channel = "ABSA",transaction_number = "Test: Small Allocation")
#> Amount too small to distribute. Depositing into the highest-priority child.
#> Withdrew: 0.09 via Allocation to Needs - Transaction ID: sys8
#> No active child accounts available.
#> Deposited: 0.09 via Allocation from Main - Transaction ID: Test: Small Allocation 
#> Deposited: 0.09 via ABSA - Transaction ID: Test: Small Allocation

✅ Summary

  • Accounts can be nested, allocated funds, and tracked independently.
  • finman intelligently handles activation, deactivation, and reallocation.
  • Periodic obligations trigger reactivation, while priorities determine allocation order.

This lifecycle ensures money flows where it’s most needed, without micromanaging every transaction.


Next: Try the tracking-obligations vignette to dive deeper into bills and debts.


💖 Sponsors

Support my work through GitHub Sponsors!

GitHub Sponsors