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. -
ChildAccount
s receive allocations from the main account (group of obligations). -
GrandchildAccount
s 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.