1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-19 13:19:39 +02:00

Rubocop updates

This commit is contained in:
Zach Gollwitzer 2024-08-22 18:21:22 -04:00
parent 5ea2fd5a23
commit 05b0b8f3a4
22 changed files with 477 additions and 476 deletions

View file

@ -1,12 +1,13 @@
# Omakase Ruby styling for Rails
inherit_gem: { rubocop-rails-omakase: rubocop.yml } inherit_gem: { rubocop-rails-omakase: rubocop.yml }
# Overwrite or add rules to create your own house style
#
# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]`
# Layout/SpaceInsideArrayLiteralBrackets:
# Enabled: false
Layout/ElseAlignment: Layout/ElseAlignment:
Enabled: false Enabled: false
Layout/EndAlignment: Layout/EndAlignment:
Enabled: false Enabled: false
Layout/IndentationConsistency:
Enabled: true
Layout/IndentationWidth:
Enabled: true
Width: 2

View file

@ -6,17 +6,17 @@ class Category::DropdownsController < ApplicationController
end end
private private
def set_from_params def set_from_params
if params[:category_id] if params[:category_id]
@selected_category = categories_scope.find(params[:category_id]) @selected_category = categories_scope.find(params[:category_id])
end
if params[:transaction_id]
@transaction = Current.family.transactions.find(params[:transaction_id])
end
end end
if params[:transaction_id] def categories_scope
@transaction = Current.family.transactions.find(params[:transaction_id]) Current.family.categories.alphabetically
end end
end
def categories_scope
Current.family.categories.alphabetically
end
end end

View file

@ -13,27 +13,27 @@ module Authentication
private private
def authenticate_user! def authenticate_user!
if user = User.find_by(id: session[:user_id]) if user = User.find_by(id: session[:user_id])
Current.user = user Current.user = user
else else
redirect_to new_session_url redirect_to new_session_url
end
end end
end
def login(user) def login(user)
Current.user = user Current.user = user
reset_session reset_session
session[:user_id] = user.id session[:user_id] = user.id
set_last_login_at set_last_login_at
end end
def logout def logout
Current.user = nil Current.user = nil
reset_session reset_session
end end
def set_last_login_at def set_last_login_at
Current.user.update(last_login_at: DateTime.now) Current.user.update(last_login_at: DateTime.now)
end end
end end

View file

@ -1,9 +1,9 @@
module Filterable module Filterable
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
before_action :set_period before_action :set_period
end end
private private

View file

@ -31,11 +31,11 @@ class MerchantsController < ApplicationController
private private
def set_merchant def set_merchant
@merchant = Current.family.merchants.find(params[:id]) @merchant = Current.family.merchants.find(params[:id])
end end
def merchant_params def merchant_params
params.require(:merchant).permit(:name, :color) params.require(:merchant).permit(:name, :color)
end end
end end

View file

@ -33,12 +33,12 @@ class PasswordResetsController < ApplicationController
private private
def set_user_by_token def set_user_by_token
@user = User.find_by_token_for(:password_reset, params[:token]) @user = User.find_by_token_for(:password_reset, params[:token])
redirect_to new_password_reset_path, alert: t("password_resets.update.invalid_token") unless @user.present? redirect_to new_password_reset_path, alert: t("password_resets.update.invalid_token") unless @user.present?
end end
def password_params def password_params
params.require(:user).permit(:password, :password_confirmation) params.require(:user).permit(:password, :password_confirmation)
end end
end end

View file

@ -12,7 +12,7 @@ class PasswordsController < ApplicationController
private private
def password_params def password_params
params.require(:user).permit(:password, :password_confirmation, :password_challenge).with_defaults(password_challenge: "") params.require(:user).permit(:password, :password_confirmation, :password_challenge).with_defaults(password_challenge: "")
end end
end end

View file

@ -28,17 +28,17 @@ class RegistrationsController < ApplicationController
private private
def set_user def set_user
@user = User.new user_params.except(:invite_code) @user = User.new user_params.except(:invite_code)
end end
def user_params def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation, :invite_code) params.require(:user).permit(:name, :email, :password, :password_confirmation, :invite_code)
end end
def claim_invite_code def claim_invite_code
unless InviteCode.claim! params[:user][:invite_code] unless InviteCode.claim! params[:user][:invite_code]
redirect_to new_registration_path, alert: t("registrations.create.invalid_invite_code") redirect_to new_registration_path, alert: t("registrations.create.invalid_invite_code")
end
end end
end
end end

View file

@ -32,8 +32,8 @@ class Settings::ProfilesController < SettingsController
private private
def user_params def user_params
params.require(:user).permit(:first_name, :last_name, :profile_image, params.require(:user).permit(:first_name, :last_name, :profile_image,
family_attributes: [ :name, :id ]) family_attributes: [ :name, :id ])
end end
end end

View file

@ -5,12 +5,12 @@ class ApplicationMailer < ActionMailer::Base
private private
def set_self_host_settings def set_self_host_settings
mail.from = Setting.email_sender mail.from = Setting.email_sender
mail.delivery_method.settings.merge!({ address: Setting.smtp_host, mail.delivery_method.settings.merge!({ address: Setting.smtp_host,
port: Setting.smtp_port, port: Setting.smtp_port,
user_name: Setting.smtp_username, user_name: Setting.smtp_username,
password: Setting.smtp_password, password: Setting.smtp_password,
tls: ENV.fetch("SMTP_TLS_ENABLED", "true") == "true" }) tls: ENV.fetch("SMTP_TLS_ENABLED", "true") == "true" })
end end
end end

View file

@ -1,9 +1,9 @@
class Account::Balance < ApplicationRecord class Account::Balance < ApplicationRecord
include Monetizable include Monetizable
belongs_to :account belongs_to :account
validates :account, :date, :balance, presence: true validates :account, :date, :balance, presence: true
monetize :balance monetize :balance
scope :in_period, ->(period) { period.date_range.nil? ? all : where(date: period.date_range) } scope :in_period, ->(period) { period.date_range.nil? ? all : where(date: period.date_range) }
scope :chronological, -> { order(:date) } scope :chronological, -> { order(:date) }
end end

View file

@ -47,7 +47,7 @@ class Category < ApplicationRecord
private private
def clear_internal_category def clear_internal_category
self.internal_category = nil self.internal_category = nil
end end
end end

View file

@ -1,14 +1,14 @@
module Monetizable module Monetizable
extend ActiveSupport::Concern extend ActiveSupport::Concern
class_methods do class_methods do
def monetize(*fields) def monetize(*fields)
fields.each do |field| fields.each do |field|
define_method("#{field}_money") do define_method("#{field}_money") do
value = self.send(field) value = self.send(field)
value.nil? ? nil : Money.new(value, currency || Money.default_currency) value.nil? ? nil : Money.new(value, currency || Money.default_currency)
end
end end
end end
end end
end
end end

View file

@ -16,10 +16,10 @@ class InviteCode < ApplicationRecord
private private
def generate_token def generate_token
loop do loop do
self.token = SecureRandom.hex(4) self.token = SecureRandom.hex(4)
break token unless self.class.exists?(token: token) break token unless self.class.exists?(token: token)
end
end end
end
end end

View file

@ -1,35 +1,35 @@
class Period class Period
attr_reader :name, :date_range attr_reader :name, :date_range
def self.find_by_name(name) def self.find_by_name(name)
INDEX[name] INDEX[name]
end end
def self.names def self.names
INDEX.keys.sort INDEX.keys.sort
end end
def initialize(name: "custom", date_range:) def initialize(name: "custom", date_range:)
@name = name @name = name
@date_range = date_range @date_range = date_range
end end
def extend_backward(duration) def extend_backward(duration)
Period.new(name: name + "_extended", date_range: (date_range.first - duration)..date_range.last) Period.new(name: name + "_extended", date_range: (date_range.first - duration)..date_range.last)
end end
BUILTIN = [ BUILTIN = [
new(name: "all", date_range: nil..Date.current), new(name: "all", date_range: nil..Date.current),
new(name: "last_7_days", date_range: 7.days.ago.to_date..Date.current), new(name: "last_7_days", date_range: 7.days.ago.to_date..Date.current),
new(name: "last_30_days", date_range: 30.days.ago.to_date..Date.current), new(name: "last_30_days", date_range: 30.days.ago.to_date..Date.current),
new(name: "last_365_days", date_range: 365.days.ago.to_date..Date.current) new(name: "last_365_days", date_range: 365.days.ago.to_date..Date.current)
] ]
INDEX = BUILTIN.index_by(&:name) INDEX = BUILTIN.index_by(&:name)
BUILTIN.each do |period| BUILTIN.each do |period|
define_singleton_method(period.name) do define_singleton_method(period.name) do
period period
end
end end
end
end end

View file

@ -16,11 +16,11 @@ class Provider::Github
latest_commit = Octokit.branch(repo, branch) latest_commit = Octokit.branch(repo, branch)
release_info = if latest_release release_info = if latest_release
{ {
version: latest_version, version: latest_version,
url: latest_release.html_url, url: latest_release.html_url,
commit_sha: Octokit.commit(repo, latest_release.tag_name).sha commit_sha: Octokit.commit(repo, latest_release.tag_name).sha
} }
end end
commit_info = { commit_info = {

View file

@ -74,18 +74,18 @@ class User < ApplicationRecord
private private
def last_user_in_family? def last_user_in_family?
family.users.count == 1 family.users.count == 1
end end
def deactivated_email def deactivated_email
email.gsub(/@/, "-deactivated-#{SecureRandom.uuid}@") email.gsub(/@/, "-deactivated-#{SecureRandom.uuid}@")
end end
def profile_image_size def profile_image_size
if profile_image.attached? && profile_image.byte_size > 5.megabytes if profile_image.attached? && profile_image.byte_size > 5.megabytes
# i18n-tasks-use t('activerecord.errors.models.user.attributes.profile_image.invalid_file_size') # i18n-tasks-use t('activerecord.errors.models.user.attributes.profile_image.invalid_file_size')
errors.add(:profile_image, :invalid_file_size, max_megabytes: 5) errors.add(:profile_image, :invalid_file_size, max_megabytes: 5)
end
end end
end
end end

View file

@ -3,100 +3,100 @@
attr_reader :name, :children, :value, :currency attr_reader :name, :children, :value, :currency
def initialize(name, currency = Money.default_currency) def initialize(name, currency = Money.default_currency)
@name = name @name = name
@currency = Money::Currency.new(currency) @currency = Money::Currency.new(currency)
@children = [] @children = []
end end
def sum def sum
return value if is_value_node? return value if is_value_node?
return Money.new(0, currency) if children.empty? && value.nil? return Money.new(0, currency) if children.empty? && value.nil?
children.sum(&:sum) children.sum(&:sum)
end end
def avg def avg
return value if is_value_node? return value if is_value_node?
return Money.new(0, currency) if children.empty? && value.nil? return Money.new(0, currency) if children.empty? && value.nil?
leaf_values = value_nodes.map(&:value) leaf_values = value_nodes.map(&:value)
leaf_values.compact.sum / leaf_values.compact.size leaf_values.compact.sum / leaf_values.compact.size
end end
def series def series
return @series if is_value_node? return @series if is_value_node?
summed_by_date = children.each_with_object(Hash.new(0)) do |child, acc| summed_by_date = children.each_with_object(Hash.new(0)) do |child, acc|
child.series.values.each do |series_value| child.series.values.each do |series_value|
acc[series_value.date] += series_value.value acc[series_value.date] += series_value.value
end
end end
end
summed_series = summed_by_date.map { |date, value| { date: date, value: value } } summed_series = summed_by_date.map { |date, value| { date: date, value: value } }
TimeSeries.new(summed_series) TimeSeries.new(summed_series)
end end
def series=(series) def series=(series)
raise "Cannot set series on a non-leaf node" unless is_value_node? raise "Cannot set series on a non-leaf node" unless is_value_node?
_series = series || TimeSeries.new([]) _series = series || TimeSeries.new([])
raise "Series must be an instance of TimeSeries" unless _series.is_a?(TimeSeries) raise "Series must be an instance of TimeSeries" unless _series.is_a?(TimeSeries)
raise "Series must contain money values in the node's currency" unless _series.values.all? { |v| v.value.currency == currency } raise "Series must contain money values in the node's currency" unless _series.values.all? { |v| v.value.currency == currency }
@series = _series @series = _series
end end
def value_nodes def value_nodes
return [ self ] unless value.nil? return [ self ] unless value.nil?
children.flat_map { |child| child.value_nodes } children.flat_map { |child| child.value_nodes }
end end
def empty? def empty?
value_nodes.empty? value_nodes.empty?
end end
def percent_of_total def percent_of_total
return 100 if parent.nil? || parent.sum.zero? return 100 if parent.nil? || parent.sum.zero?
((sum / parent.sum) * 100).round(1) ((sum / parent.sum) * 100).round(1)
end end
def add_child_group(name, currency = Money.default_currency) def add_child_group(name, currency = Money.default_currency)
raise "Cannot add subgroup to node with a value" if is_value_node? raise "Cannot add subgroup to node with a value" if is_value_node?
child = self.class.new(name, currency) child = self.class.new(name, currency)
child.parent = self child.parent = self
@children << child @children << child
child child
end end
def add_value_node(original, value, series = nil) def add_value_node(original, value, series = nil)
raise "Cannot add value node to a non-leaf node" unless can_add_value_node? raise "Cannot add value node to a non-leaf node" unless can_add_value_node?
child = self.class.new(original.name) child = self.class.new(original.name)
child.original = original child.original = original
child.value = value child.value = value
child.series = series child.series = series
child.parent = self child.parent = self
@children << child @children << child
child child
end end
def value=(value) def value=(value)
raise "Cannot set value on a non-leaf node" unless is_leaf_node? raise "Cannot set value on a non-leaf node" unless is_leaf_node?
raise "Value must be an instance of Money" unless value.is_a?(Money) raise "Value must be an instance of Money" unless value.is_a?(Money)
@value = value @value = value
@currency = value.currency @currency = value.currency
end end
def is_leaf_node? def is_leaf_node?
children.empty? children.empty?
end end
def is_value_node? def is_value_node?
value.present? value.present?
end end
private private
def can_add_value_node? def can_add_value_node?
return false if is_value_node? return false if is_value_node?
children.empty? || children.all?(&:is_value_node?) children.empty? || children.all?(&:is_value_node?)
end end
end end

View file

@ -1,62 +1,62 @@
module Money::Arithmetic module Money::Arithmetic
CoercedNumeric = Struct.new(:value) CoercedNumeric = Struct.new(:value)
def +(other) def +(other)
if other.is_a?(Money) if other.is_a?(Money)
self.class.new(amount + other.amount, currency) self.class.new(amount + other.amount, currency)
else else
value = other.is_a?(CoercedNumeric) ? other.value : other value = other.is_a?(CoercedNumeric) ? other.value : other
self.class.new(amount + value, currency) self.class.new(amount + value, currency)
end
end end
end
def -(other) def -(other)
if other.is_a?(Money) if other.is_a?(Money)
self.class.new(amount - other.amount, currency) self.class.new(amount - other.amount, currency)
else else
value = other.is_a?(CoercedNumeric) ? other.value : other value = other.is_a?(CoercedNumeric) ? other.value : other
self.class.new(amount - value, currency) self.class.new(amount - value, currency)
end
end end
end
def -@ def -@
self.class.new(-amount, currency) self.class.new(-amount, currency)
end end
def *(other) def *(other)
raise TypeError, "Can't multiply Money by Money, use Numeric instead" if other.is_a?(self.class) raise TypeError, "Can't multiply Money by Money, use Numeric instead" if other.is_a?(self.class)
value = other.is_a?(CoercedNumeric) ? other.value : other value = other.is_a?(CoercedNumeric) ? other.value : other
self.class.new(amount * value, currency) self.class.new(amount * value, currency)
end end
def /(other) def /(other)
if other.is_a?(self.class) if other.is_a?(self.class)
amount / other.amount amount / other.amount
else else
raise TypeError, "can't divide Numeric by Money" if other.is_a?(CoercedNumeric) raise TypeError, "can't divide Numeric by Money" if other.is_a?(CoercedNumeric)
self.class.new(amount / other, currency) self.class.new(amount / other, currency)
end
end end
end
def abs def abs
self.class.new(amount.abs, currency) self.class.new(amount.abs, currency)
end end
def zero? def zero?
amount.zero? amount.zero?
end end
def negative? def negative?
amount.negative? amount.negative?
end end
def positive? def positive?
amount.positive? amount.positive?
end end
# Override Ruby's coerce method so the order of operands doesn't matter # Override Ruby's coerce method so the order of operands doesn't matter
# Wrap in Coerced so we can distinguish between Money and other types # Wrap in Coerced so we can distinguish between Money and other types
def coerce(other) def coerce(other)
[ self, CoercedNumeric.new(other) ] [ self, CoercedNumeric.new(other) ]
end end
end end

View file

@ -1,69 +1,69 @@
class Money::Currency class Money::Currency
include Comparable include Comparable
class UnknownCurrencyError < ArgumentError; end class UnknownCurrencyError < ArgumentError; end
CURRENCIES_FILE_PATH = Rails.root.join("config", "currencies.yml") CURRENCIES_FILE_PATH = Rails.root.join("config", "currencies.yml")
# Cached instances by iso code # Cached instances by iso code
@@instances = {} @@instances = {}
class << self class << self
def new(object) def new(object)
iso_code = case object iso_code = case object
when String, Symbol when String, Symbol
object.to_s.downcase object.to_s.downcase
when Money::Currency when Money::Currency
object.iso_code.downcase object.iso_code.downcase
else else
raise ArgumentError, "Invalid argument type" raise ArgumentError, "Invalid argument type"
end
@@instances[iso_code] ||= super(iso_code)
end end
def all @@instances[iso_code] ||= super(iso_code)
@all ||= YAML.load_file(CURRENCIES_FILE_PATH)
end
def all_instances
all.values.map { |currency_data| new(currency_data["iso_code"]) }
end
def popular
all.values.sort_by { |currency| currency["priority"] }.first(12).map { |currency_data| new(currency_data["iso_code"]) }
end
end end
attr_reader :name, :priority, :iso_code, :iso_numeric, :html_code, def all
:symbol, :minor_unit, :minor_unit_conversion, :smallest_denomination, @all ||= YAML.load_file(CURRENCIES_FILE_PATH)
:separator, :delimiter, :default_format, :default_precision
def initialize(iso_code)
currency_data = self.class.all[iso_code]
raise UnknownCurrencyError if currency_data.nil?
@name = currency_data["name"]
@priority = currency_data["priority"]
@iso_code = currency_data["iso_code"]
@iso_numeric = currency_data["iso_numeric"]
@html_code = currency_data["html_code"]
@symbol = currency_data["symbol"]
@minor_unit = currency_data["minor_unit"]
@minor_unit_conversion = currency_data["minor_unit_conversion"]
@smallest_denomination = currency_data["smallest_denomination"]
@separator = currency_data["separator"]
@delimiter = currency_data["delimiter"]
@default_format = currency_data["default_format"]
@default_precision = currency_data["default_precision"]
end end
def step def all_instances
(1.0/10**default_precision) all.values.map { |currency_data| new(currency_data["iso_code"]) }
end end
def <=>(other) def popular
return nil unless other.is_a?(Money::Currency) all.values.sort_by { |currency| currency["priority"] }.first(12).map { |currency_data| new(currency_data["iso_code"]) }
@iso_code <=> other.iso_code
end end
end
attr_reader :name, :priority, :iso_code, :iso_numeric, :html_code,
:symbol, :minor_unit, :minor_unit_conversion, :smallest_denomination,
:separator, :delimiter, :default_format, :default_precision
def initialize(iso_code)
currency_data = self.class.all[iso_code]
raise UnknownCurrencyError if currency_data.nil?
@name = currency_data["name"]
@priority = currency_data["priority"]
@iso_code = currency_data["iso_code"]
@iso_numeric = currency_data["iso_numeric"]
@html_code = currency_data["html_code"]
@symbol = currency_data["symbol"]
@minor_unit = currency_data["minor_unit"]
@minor_unit_conversion = currency_data["minor_unit_conversion"]
@smallest_denomination = currency_data["smallest_denomination"]
@separator = currency_data["separator"]
@delimiter = currency_data["delimiter"]
@default_format = currency_data["default_format"]
@default_precision = currency_data["default_precision"]
end
def step
(1.0/10**default_precision)
end
def <=>(other)
return nil unless other.is_a?(Money::Currency)
@iso_code <=> other.iso_code
end
end end

View file

@ -1,53 +1,53 @@
require "test_helper" require "test_helper"
class Money::CurrencyTest < ActiveSupport::TestCase class Money::CurrencyTest < ActiveSupport::TestCase
setup do setup do
@currency = Money::Currency.new(:usd) @currency = Money::Currency.new(:usd)
end end
test "has many currencies" do test "has many currencies" do
assert_operator Money::Currency.all.count, :>, 100 assert_operator Money::Currency.all.count, :>, 100
end end
test "can test equality of currencies" do test "can test equality of currencies" do
assert_equal Money::Currency.new(:usd), Money::Currency.new(:usd) assert_equal Money::Currency.new(:usd), Money::Currency.new(:usd)
assert_not_equal Money::Currency.new(:usd), Money::Currency.new(:eur) assert_not_equal Money::Currency.new(:usd), Money::Currency.new(:eur)
end end
test "can get metadata about a currency" do test "can get metadata about a currency" do
assert_equal "USD", @currency.iso_code assert_equal "USD", @currency.iso_code
assert_equal "United States Dollar", @currency.name assert_equal "United States Dollar", @currency.name
assert_equal "$", @currency.symbol assert_equal "$", @currency.symbol
assert_equal 1, @currency.priority assert_equal 1, @currency.priority
assert_equal "Cent", @currency.minor_unit assert_equal "Cent", @currency.minor_unit
assert_equal 100, @currency.minor_unit_conversion assert_equal 100, @currency.minor_unit_conversion
assert_equal 1, @currency.smallest_denomination assert_equal 1, @currency.smallest_denomination
assert_equal ".", @currency.separator assert_equal ".", @currency.separator
assert_equal ",", @currency.delimiter assert_equal ",", @currency.delimiter
assert_equal "%u%n", @currency.default_format assert_equal "%u%n", @currency.default_format
assert_equal 2, @currency.default_precision assert_equal 2, @currency.default_precision
end end
test "can extract cents string from amount" do test "can extract cents string from amount" do
value1 = Money.new(100) value1 = Money.new(100)
value2 = Money.new(100.1) value2 = Money.new(100.1)
value3 = Money.new(100.12) value3 = Money.new(100.12)
value4 = Money.new(100.123) value4 = Money.new(100.123)
value5 = Money.new(200, :jpy) value5 = Money.new(200, :jpy)
assert_equal "00", value1.cents_str assert_equal "00", value1.cents_str
assert_equal "10", value2.cents_str assert_equal "10", value2.cents_str
assert_equal "12", value3.cents_str assert_equal "12", value3.cents_str
assert_equal "12", value4.cents_str assert_equal "12", value4.cents_str
assert_equal "", value5.cents_str assert_equal "", value5.cents_str
assert_equal "", value4.cents_str(0) assert_equal "", value4.cents_str(0)
assert_equal "1", value4.cents_str(1) assert_equal "1", value4.cents_str(1)
assert_equal "12", value4.cents_str(2) assert_equal "12", value4.cents_str(2)
assert_equal "123", value4.cents_str(3) assert_equal "123", value4.cents_str(3)
end end
test "step returns the smallest value of the currency" do test "step returns the smallest value of the currency" do
assert_equal 0.01, @currency.step assert_equal 0.01, @currency.step
end end
end end

View file

@ -1,141 +1,141 @@
require "test_helper" require "test_helper"
require "ostruct" require "ostruct"
class ValueGroupTest < ActiveSupport::TestCase class ValueGroupTest < ActiveSupport::TestCase
setup do setup do
# Level 1 # Level 1
@assets = ValueGroup.new("Assets", :usd) @assets = ValueGroup.new("Assets", :usd)
# Level 2 # Level 2
@depositories = @assets.add_child_group("Depositories", :usd) @depositories = @assets.add_child_group("Depositories", :usd)
@other_assets = @assets.add_child_group("Other Assets", :usd) @other_assets = @assets.add_child_group("Other Assets", :usd)
# Level 3 (leaf/value nodes) # Level 3 (leaf/value nodes)
@checking_node = @depositories.add_value_node(OpenStruct.new({ name: "Checking", value: Money.new(5000) }), Money.new(5000)) @checking_node = @depositories.add_value_node(OpenStruct.new({ name: "Checking", value: Money.new(5000) }), Money.new(5000))
@savings_node = @depositories.add_value_node(OpenStruct.new({ name: "Savings", value: Money.new(20000) }), Money.new(20000)) @savings_node = @depositories.add_value_node(OpenStruct.new({ name: "Savings", value: Money.new(20000) }), Money.new(20000))
@collectable_node = @other_assets.add_value_node(OpenStruct.new({ name: "Collectable", value: Money.new(550) }), Money.new(550)) @collectable_node = @other_assets.add_value_node(OpenStruct.new({ name: "Collectable", value: Money.new(550) }), Money.new(550))
end
test "empty group works" do
group = ValueGroup.new("Root", :usd)
assert_equal "Root", group.name
assert_equal [], group.children
assert_equal 0, group.sum
assert_equal 0, group.avg
assert_equal 100, group.percent_of_total
assert_nil group.parent
end
test "group without value nodes has no value" do
assets = ValueGroup.new("Assets")
depositories = assets.add_child_group("Depositories")
assert_equal 0, assets.sum
assert_equal 0, depositories.sum
end
test "sum equals value at leaf level" do
assert_equal @checking_node.value, @checking_node.sum
assert_equal @savings_node.value, @savings_node.sum
assert_equal @collectable_node.value, @collectable_node.sum
end
test "value is nil at rollup levels" do
assert_not_equal @depositories.value, @depositories.sum
assert_nil @depositories.value
assert_nil @other_assets.value
end
test "generates list of value nodes regardless of level in hierarchy" do
assert_equal [ @checking_node, @savings_node, @collectable_node ], @assets.value_nodes
assert_equal [ @checking_node, @savings_node ], @depositories.value_nodes
assert_equal [ @collectable_node ], @other_assets.value_nodes
end
test "group with value nodes aggregates totals correctly" do
assert_equal Money.new(5000), @checking_node.sum
assert_equal Money.new(20000), @savings_node.sum
assert_equal Money.new(550), @collectable_node.sum
assert_equal Money.new(25000), @depositories.sum
assert_equal Money.new(550), @other_assets.sum
assert_equal Money.new(25550), @assets.sum
end
test "group averages leaf nodes" do
assert_equal Money.new(5000), @checking_node.avg
assert_equal Money.new(20000), @savings_node.avg
assert_equal Money.new(550), @collectable_node.avg
assert_in_delta 12500, @depositories.avg.amount, 0.01
assert_in_delta 550, @other_assets.avg.amount, 0.01
assert_in_delta 8516.67, @assets.avg.amount, 0.01
end
# Percentage of parent group (i.e. collectable is 100% of "Other Assets" group)
test "group calculates percent of parent total" do
assert_equal 100, @assets.percent_of_total
assert_in_delta 97.85, @depositories.percent_of_total, 0.1
assert_in_delta 2.15, @other_assets.percent_of_total, 0.1
assert_in_delta 80.0, @savings_node.percent_of_total, 0.1
assert_in_delta 20.0, @checking_node.percent_of_total, 0.1
assert_equal 100, @collectable_node.percent_of_total
end
test "handles unbalanced tree" do
vehicles = @assets.add_child_group("Vehicles")
# Since we didn't add any value nodes to vehicles, shouldn't affect rollups
assert_equal Money.new(25550), @assets.sum
end
test "can attach and aggregate time series" do
checking_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(4000) }, { date: Date.current, value: Money.new(5000) } ])
savings_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(19000) }, { date: Date.current, value: Money.new(20000) } ])
@checking_node.series = checking_series
@savings_node.series = savings_series
assert_not_nil @checking_node.series
assert_not_nil @savings_node.series
assert_equal @checking_node.sum, @checking_node.series.last.value
assert_equal @savings_node.sum, @savings_node.series.last.value
aggregated_depository_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(23000) }, { date: Date.current, value: Money.new(25000) } ])
aggregated_assets_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(23000) }, { date: Date.current, value: Money.new(25000) } ])
assert_equal aggregated_depository_series.values, @depositories.series.values
assert_equal aggregated_assets_series.values, @assets.series.values
end
test "attached series must be a TimeSeries" do
assert_raises(RuntimeError) do
@checking_node.series = []
end
end
test "cannot add time series to non-leaf node" do
assert_raises(RuntimeError) do
@assets.series = TimeSeries.new([])
end
end
test "can only add value node at leaf level of tree" do
root = ValueGroup.new("Root Level")
grandparent = root.add_child_group("Grandparent")
parent = grandparent.add_child_group("Parent")
value_node = parent.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
assert_raises(RuntimeError) do
value_node.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
end end
test "empty group works" do assert_raises(RuntimeError) do
group = ValueGroup.new("Root", :usd) grandparent.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
assert_equal "Root", group.name
assert_equal [], group.children
assert_equal 0, group.sum
assert_equal 0, group.avg
assert_equal 100, group.percent_of_total
assert_nil group.parent
end
test "group without value nodes has no value" do
assets = ValueGroup.new("Assets")
depositories = assets.add_child_group("Depositories")
assert_equal 0, assets.sum
assert_equal 0, depositories.sum
end
test "sum equals value at leaf level" do
assert_equal @checking_node.value, @checking_node.sum
assert_equal @savings_node.value, @savings_node.sum
assert_equal @collectable_node.value, @collectable_node.sum
end
test "value is nil at rollup levels" do
assert_not_equal @depositories.value, @depositories.sum
assert_nil @depositories.value
assert_nil @other_assets.value
end
test "generates list of value nodes regardless of level in hierarchy" do
assert_equal [ @checking_node, @savings_node, @collectable_node ], @assets.value_nodes
assert_equal [ @checking_node, @savings_node ], @depositories.value_nodes
assert_equal [ @collectable_node ], @other_assets.value_nodes
end
test "group with value nodes aggregates totals correctly" do
assert_equal Money.new(5000), @checking_node.sum
assert_equal Money.new(20000), @savings_node.sum
assert_equal Money.new(550), @collectable_node.sum
assert_equal Money.new(25000), @depositories.sum
assert_equal Money.new(550), @other_assets.sum
assert_equal Money.new(25550), @assets.sum
end
test "group averages leaf nodes" do
assert_equal Money.new(5000), @checking_node.avg
assert_equal Money.new(20000), @savings_node.avg
assert_equal Money.new(550), @collectable_node.avg
assert_in_delta 12500, @depositories.avg.amount, 0.01
assert_in_delta 550, @other_assets.avg.amount, 0.01
assert_in_delta 8516.67, @assets.avg.amount, 0.01
end
# Percentage of parent group (i.e. collectable is 100% of "Other Assets" group)
test "group calculates percent of parent total" do
assert_equal 100, @assets.percent_of_total
assert_in_delta 97.85, @depositories.percent_of_total, 0.1
assert_in_delta 2.15, @other_assets.percent_of_total, 0.1
assert_in_delta 80.0, @savings_node.percent_of_total, 0.1
assert_in_delta 20.0, @checking_node.percent_of_total, 0.1
assert_equal 100, @collectable_node.percent_of_total
end
test "handles unbalanced tree" do
vehicles = @assets.add_child_group("Vehicles")
# Since we didn't add any value nodes to vehicles, shouldn't affect rollups
assert_equal Money.new(25550), @assets.sum
end
test "can attach and aggregate time series" do
checking_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(4000) }, { date: Date.current, value: Money.new(5000) } ])
savings_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(19000) }, { date: Date.current, value: Money.new(20000) } ])
@checking_node.series = checking_series
@savings_node.series = savings_series
assert_not_nil @checking_node.series
assert_not_nil @savings_node.series
assert_equal @checking_node.sum, @checking_node.series.last.value
assert_equal @savings_node.sum, @savings_node.series.last.value
aggregated_depository_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(23000) }, { date: Date.current, value: Money.new(25000) } ])
aggregated_assets_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(23000) }, { date: Date.current, value: Money.new(25000) } ])
assert_equal aggregated_depository_series.values, @depositories.series.values
assert_equal aggregated_assets_series.values, @assets.series.values
end
test "attached series must be a TimeSeries" do
assert_raises(RuntimeError) do
@checking_node.series = []
end
end
test "cannot add time series to non-leaf node" do
assert_raises(RuntimeError) do
@assets.series = TimeSeries.new([])
end
end
test "can only add value node at leaf level of tree" do
root = ValueGroup.new("Root Level")
grandparent = root.add_child_group("Grandparent")
parent = grandparent.add_child_group("Parent")
value_node = parent.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
assert_raises(RuntimeError) do
value_node.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
end
assert_raises(RuntimeError) do
grandparent.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
end
end end
end
end end