2025-04-14 11:40:34 -04:00
|
|
|
class Valuation < ApplicationRecord
|
|
|
|
include Entryable
|
2025-07-07 11:31:37 -04:00
|
|
|
|
|
|
|
enum :kind, {
|
|
|
|
recon: "recon", # A balance reconciliation that sets the Account balance from this point forward (often defined by user)
|
|
|
|
snapshot: "snapshot", # An "event-sourcing snapshot", which is purely for performance so less history is required to derive the balance
|
|
|
|
opening_anchor: "opening_anchor", # Each account has a single opening anchor, which defines the opening balance on the account
|
|
|
|
current_anchor: "current_anchor" # Each account has a single current anchor, which defines the current balance on the account
|
|
|
|
}, validate: true
|
|
|
|
|
|
|
|
# Each account can have at most 1 opening anchor and 1 current anchor. All valuations between these anchors should
|
|
|
|
# be either "recon" or "snapshot". This ensures we can reliably construct the account balance history solely from Entries.
|
|
|
|
validate :unique_anchor_per_account, if: -> { opening_anchor? || current_anchor? }
|
2025-07-08 10:25:16 -04:00
|
|
|
validate :manual_accounts_cannot_have_current_anchor
|
2025-07-07 11:31:37 -04:00
|
|
|
|
|
|
|
private
|
|
|
|
def unique_anchor_per_account
|
|
|
|
return unless entry&.account
|
|
|
|
|
|
|
|
existing_anchor = entry.account.valuations
|
|
|
|
.joins(:entry)
|
|
|
|
.where(kind: kind)
|
|
|
|
.where.not(id: id)
|
|
|
|
.exists?
|
|
|
|
|
|
|
|
if existing_anchor
|
|
|
|
errors.add(:kind, "#{kind.humanize} already exists for this account")
|
|
|
|
end
|
|
|
|
end
|
2025-07-08 10:25:16 -04:00
|
|
|
|
|
|
|
def manual_accounts_cannot_have_current_anchor
|
|
|
|
return unless entry&.account
|
|
|
|
|
|
|
|
if entry.account.unlinked? && current_anchor?
|
|
|
|
errors.add(:kind, "Manual accounts cannot have a current anchor")
|
|
|
|
end
|
|
|
|
end
|
2025-04-14 11:40:34 -04:00
|
|
|
end
|