1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-07 14:35:23 +02:00

Update properties controller to use new creational and update balance methods

This commit is contained in:
Zach Gollwitzer 2025-07-09 22:28:07 -04:00
parent d459ebdad8
commit 25f0c78c47
17 changed files with 500 additions and 144 deletions

View file

@ -0,0 +1,87 @@
class Account::OverviewForm
include ActiveModel::Model
attr_accessor :account, :name, :currency, :opening_date
attr_reader :opening_balance, :opening_cash_balance, :current_balance, :current_cash_balance
Result = Struct.new(:success?, :updated?, :error, keyword_init: true)
CurrencyUpdateError = Class.new(StandardError)
def opening_balance=(value)
@opening_balance = value.nil? ? nil : value.to_d
end
def opening_cash_balance=(value)
@opening_cash_balance = value.nil? ? nil : value.to_d
end
def current_balance=(value)
@current_balance = value.nil? ? nil : value.to_d
end
def current_cash_balance=(value)
@current_cash_balance = value.nil? ? nil : value.to_d
end
def save
# Validate that balance fields are properly paired
if (!opening_balance.nil? && opening_cash_balance.nil?) ||
(opening_balance.nil? && !opening_cash_balance.nil?)
raise ArgumentError, "Both opening_balance and opening_cash_balance must be provided together"
end
if (!current_balance.nil? && current_cash_balance.nil?) ||
(current_balance.nil? && !current_cash_balance.nil?)
raise ArgumentError, "Both current_balance and current_cash_balance must be provided together"
end
updated = false
sync_required = false
Account.transaction do
# Update name if provided
if name.present? && name != account.name
account.update!(name: name)
updated = true
end
# Update currency if provided
if currency.present? && currency != account.currency
account.update_currency!(currency)
updated = true
sync_required = true
end
# Update opening balance if provided (already validated that both are present)
if !opening_balance.nil?
account.set_or_update_opening_balance!(
balance: opening_balance,
cash_balance: opening_cash_balance,
date: opening_date # optional
)
updated = true
sync_required = true
end
# Update current balance if provided (already validated that both are present)
if !current_balance.nil?
account.update_current_balance!(
balance: current_balance,
cash_balance: current_cash_balance
)
updated = true
sync_required = true
end
end
# Only sync if transaction succeeded and sync is required
account.sync_later if sync_required
Result.new(success?: true, updated?: updated)
rescue ArgumentError => e
# Re-raise ArgumentError as it's a developer error
raise e
rescue => e
Result.new(success?: false, updated?: false, error: e.message)
end
end

View file

@ -17,39 +17,63 @@ module Account::Reconcileable
balance - cash_balance
end
def opening_balance
@opening_balance ||= opening_anchor_valuation&.balance
end
def opening_cash_balance
@opening_cash_balance ||= opening_anchor_valuation&.cash_balance
end
def opening_date
@opening_date ||= opening_anchor_valuation&.entry&.date
end
def reconcile_balance!(balance:, cash_balance:, date:)
raise InvalidBalanceError, "Cash balance cannot exceed balance" if cash_balance > balance
raise InvalidBalanceError, "Linked accounts cannot be reconciled" if linked?
existing_valuation = valuations.joins(:entry).where(kind: "recon", entry: { date: Date.current }).first
existing_valuation = valuations.joins(:entry).where(kind: "recon", entry: { date: date }).first
if existing_valuation.present?
existing_valuation.update!(
balance: balance,
cash_balance: cash_balance
)
else
entries.create!(
date: date,
name: Valuation::Name.new("recon", self.accountable_type),
amount: balance,
currency: self.currency,
entryable: Valuation.new(
kind: "recon",
transaction do
if existing_valuation.present?
existing_valuation.update!(
balance: balance,
cash_balance: cash_balance
)
)
else
entries.create!(
date: date,
name: Valuation::Name.new("recon", self.accountable_type),
amount: balance,
currency: self.currency,
entryable: Valuation.new(
kind: "recon",
balance: balance,
cash_balance: cash_balance
)
)
end
# Update cached balance fields on account when reconciling for current date
if date == Date.current
update!(balance: balance, cash_balance: cash_balance)
end
end
end
def update_current_balance(balance:, cash_balance:)
def update_current_balance!(balance:, cash_balance:)
raise InvalidBalanceError, "Cash balance cannot exceed balance" if cash_balance > balance
if opening_anchor_valuation.present? && valuations.where(kind: "recon").empty?
adjust_opening_balance_with_delta(balance:, cash_balance:)
else
reconcile_balance!(balance:, cash_balance:, date: Date.current)
transaction do
if opening_anchor_valuation.present? && valuations.where(kind: "recon").empty?
adjust_opening_balance_with_delta(balance:, cash_balance:)
else
reconcile_balance!(balance:, cash_balance:, date: Date.current)
end
# Always update cached balance fields when updating current balance
update!(balance: balance, cash_balance: cash_balance)
end
end
@ -100,7 +124,7 @@ module Account::Reconcileable
private
def opening_anchor_valuation
valuations.opening_anchor.first
@opening_anchor_valuation ||= valuations.opening_anchor.includes(:entry).first
end
def current_anchor_valuation

View file

@ -1,7 +1,7 @@
module Family::AccountCreatable
extend ActiveSupport::Concern
def create_property_account!(name:, current_value:, purchase_price: nil, purchase_date: nil, currency: nil)
def create_property_account!(name:, current_value:, purchase_price: nil, purchase_date: nil, currency: nil, draft: false)
create_manual_account!(
name: name,
balance: current_value,
@ -9,11 +9,12 @@ module Family::AccountCreatable
accountable_type: Property,
opening_balance: purchase_price,
opening_date: purchase_date,
currency: currency
currency: currency,
draft: draft
)
end
def create_vehicle_account!(name:, current_value:, purchase_price: nil, purchase_date: nil, currency: nil)
def create_vehicle_account!(name:, current_value:, purchase_price: nil, purchase_date: nil, currency: nil, draft: false)
create_manual_account!(
name: name,
balance: current_value,
@ -21,23 +22,25 @@ module Family::AccountCreatable
accountable_type: Vehicle,
opening_balance: purchase_price,
opening_date: purchase_date,
currency: currency
currency: currency,
draft: draft
)
end
def create_depository_account!(name:, current_balance:, opening_date: nil, currency: nil)
def create_depository_account!(name:, current_balance:, opening_date: nil, currency: nil, draft: false)
create_manual_account!(
name: name,
balance: current_balance,
cash_balance: current_balance,
accountable_type: Depository,
opening_date: opening_date,
currency: currency
currency: currency,
draft: draft
)
end
# Investment account values are built up by adding holdings / trades, not by initializing a "balance"
def create_investment_account!(name:, currency: nil)
def create_investment_account!(name:, currency: nil, draft: false)
create_manual_account!(
name: name,
balance: 0,
@ -45,11 +48,12 @@ module Family::AccountCreatable
accountable_type: Investment,
opening_balance: 0, # Investment accounts start empty
opening_cash_balance: 0,
currency: currency
currency: currency,
draft: draft
)
end
def create_other_asset_account!(name:, current_value:, purchase_price: nil, purchase_date: nil, currency: nil)
def create_other_asset_account!(name:, current_value:, purchase_price: nil, purchase_date: nil, currency: nil, draft: false)
create_manual_account!(
name: name,
balance: current_value,
@ -57,11 +61,12 @@ module Family::AccountCreatable
accountable_type: OtherAsset,
opening_balance: purchase_price,
opening_date: purchase_date,
currency: currency
currency: currency,
draft: draft
)
end
def create_other_liability_account!(name:, current_debt:, original_debt: nil, origination_date: nil, currency: nil)
def create_other_liability_account!(name:, current_debt:, original_debt: nil, origination_date: nil, currency: nil, draft: false)
create_manual_account!(
name: name,
balance: current_debt,
@ -69,12 +74,13 @@ module Family::AccountCreatable
accountable_type: OtherLiability,
opening_balance: original_debt,
opening_date: origination_date,
currency: currency
currency: currency,
draft: draft
)
end
# For now, crypto accounts are very simple; we just track overall value
def create_crypto_account!(name:, current_value:, currency: nil)
def create_crypto_account!(name:, current_value:, currency: nil, draft: false)
create_manual_account!(
name: name,
balance: current_value,
@ -82,11 +88,12 @@ module Family::AccountCreatable
accountable_type: Crypto,
opening_balance: current_value,
opening_cash_balance: current_value,
currency: currency
currency: currency,
draft: draft
)
end
def create_credit_card_account!(name:, current_debt:, opening_date: nil, currency: nil)
def create_credit_card_account!(name:, current_debt:, opening_date: nil, currency: nil, draft: false)
create_manual_account!(
name: name,
balance: current_debt,
@ -94,11 +101,12 @@ module Family::AccountCreatable
accountable_type: CreditCard,
opening_balance: 0, # Credit cards typically start with no debt
opening_date: opening_date,
currency: currency
currency: currency,
draft: draft
)
end
def create_loan_account!(name:, current_principal:, original_principal: nil, origination_date: nil, currency: nil)
def create_loan_account!(name:, current_principal:, original_principal: nil, origination_date: nil, currency: nil, draft: false)
create_manual_account!(
name: name,
balance: current_principal,
@ -106,7 +114,8 @@ module Family::AccountCreatable
accountable_type: Loan,
opening_balance: original_principal,
opening_date: origination_date,
currency: currency
currency: currency,
draft: draft
)
end
@ -128,14 +137,15 @@ module Family::AccountCreatable
private
def create_manual_account!(name:, balance:, cash_balance:, accountable_type:, opening_balance: nil, opening_cash_balance: nil, opening_date: nil, currency: nil)
def create_manual_account!(name:, balance:, cash_balance:, accountable_type:, opening_balance: nil, opening_cash_balance: nil, opening_date: nil, currency: nil, draft: false)
Family.transaction do
account = accounts.create!(
name: name,
balance: balance,
cash_balance: cash_balance,
currency: currency.presence || self.currency,
accountable: accountable_type.new
accountable: accountable_type.new,
status: draft ? "draft" : "active"
)
account.set_or_update_opening_balance!(

View file

@ -52,6 +52,7 @@ class Property < ApplicationRecord
private
def first_valuation_amount
return nil unless account
account.entries.valuations.order(:date).first&.amount_money || account.balance_money
end
end