diff --git a/.rubocop.yml b/.rubocop.yml index 990e2b9c..153e820e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,12 +1,13 @@ -# Omakase Ruby styling for Rails 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: Enabled: false Layout/EndAlignment: - Enabled: false \ No newline at end of file + Enabled: false + +Layout/IndentationConsistency: + Enabled: true + +Layout/IndentationWidth: + Enabled: true + Width: 2 \ No newline at end of file diff --git a/app/controllers/category/dropdowns_controller.rb b/app/controllers/category/dropdowns_controller.rb index ef2322e4..b8d8e69a 100644 --- a/app/controllers/category/dropdowns_controller.rb +++ b/app/controllers/category/dropdowns_controller.rb @@ -6,17 +6,17 @@ class Category::DropdownsController < ApplicationController end private - def set_from_params - if params[:category_id] - @selected_category = categories_scope.find(params[:category_id]) + def set_from_params + if 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 - if params[:transaction_id] - @transaction = Current.family.transactions.find(params[:transaction_id]) + def categories_scope + Current.family.categories.alphabetically end - end - - def categories_scope - Current.family.categories.alphabetically - end end diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index 0f1c6913..d1a70299 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -13,27 +13,27 @@ module Authentication private - def authenticate_user! - if user = User.find_by(id: session[:user_id]) - Current.user = user - else - redirect_to new_session_url + def authenticate_user! + if user = User.find_by(id: session[:user_id]) + Current.user = user + else + redirect_to new_session_url + end end - end - def login(user) - Current.user = user - reset_session - session[:user_id] = user.id - set_last_login_at - end + def login(user) + Current.user = user + reset_session + session[:user_id] = user.id + set_last_login_at + end - def logout - Current.user = nil - reset_session - end + def logout + Current.user = nil + reset_session + end - def set_last_login_at - Current.user.update(last_login_at: DateTime.now) - end + def set_last_login_at + Current.user.update(last_login_at: DateTime.now) + end end diff --git a/app/controllers/concerns/filterable.rb b/app/controllers/concerns/filterable.rb index 65ed8eed..4fe42f45 100644 --- a/app/controllers/concerns/filterable.rb +++ b/app/controllers/concerns/filterable.rb @@ -1,9 +1,9 @@ module Filterable - extend ActiveSupport::Concern + extend ActiveSupport::Concern - included do - before_action :set_period - end + included do + before_action :set_period + end private diff --git a/app/controllers/merchants_controller.rb b/app/controllers/merchants_controller.rb index 055f3da8..2efce189 100644 --- a/app/controllers/merchants_controller.rb +++ b/app/controllers/merchants_controller.rb @@ -31,11 +31,11 @@ class MerchantsController < ApplicationController private - def set_merchant - @merchant = Current.family.merchants.find(params[:id]) - end + def set_merchant + @merchant = Current.family.merchants.find(params[:id]) + end - def merchant_params - params.require(:merchant).permit(:name, :color) - end + def merchant_params + params.require(:merchant).permit(:name, :color) + end end diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb index 6e86eb9d..09dea955 100644 --- a/app/controllers/password_resets_controller.rb +++ b/app/controllers/password_resets_controller.rb @@ -33,12 +33,12 @@ class PasswordResetsController < ApplicationController private - def set_user_by_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? - end + def set_user_by_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? + end - def password_params - params.require(:user).permit(:password, :password_confirmation) - end + def password_params + params.require(:user).permit(:password, :password_confirmation) + end end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index eac86a07..83bc6cbe 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -12,7 +12,7 @@ class PasswordsController < ApplicationController private - def password_params - params.require(:user).permit(:password, :password_confirmation, :password_challenge).with_defaults(password_challenge: "") - end + def password_params + params.require(:user).permit(:password, :password_confirmation, :password_challenge).with_defaults(password_challenge: "") + end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 3a18477d..af628048 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -28,17 +28,17 @@ class RegistrationsController < ApplicationController private - def set_user - @user = User.new user_params.except(:invite_code) - end - - def user_params - params.require(:user).permit(:name, :email, :password, :password_confirmation, :invite_code) - end - - def claim_invite_code - unless InviteCode.claim! params[:user][:invite_code] - redirect_to new_registration_path, alert: t("registrations.create.invalid_invite_code") + def set_user + @user = User.new user_params.except(:invite_code) + end + + def user_params + params.require(:user).permit(:name, :email, :password, :password_confirmation, :invite_code) + end + + def claim_invite_code + unless InviteCode.claim! params[:user][:invite_code] + redirect_to new_registration_path, alert: t("registrations.create.invalid_invite_code") + end end - end end diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index a2c6b219..e8f135ba 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -32,8 +32,8 @@ class Settings::ProfilesController < SettingsController private - def user_params - params.require(:user).permit(:first_name, :last_name, :profile_image, - family_attributes: [ :name, :id ]) - end + def user_params + params.require(:user).permit(:first_name, :last_name, :profile_image, + family_attributes: [ :name, :id ]) + end end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 3c23f0d4..adc394b4 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -5,12 +5,12 @@ class ApplicationMailer < ActionMailer::Base private - def set_self_host_settings - mail.from = Setting.email_sender - mail.delivery_method.settings.merge!({ address: Setting.smtp_host, - port: Setting.smtp_port, - user_name: Setting.smtp_username, - password: Setting.smtp_password, - tls: ENV.fetch("SMTP_TLS_ENABLED", "true") == "true" }) - end + def set_self_host_settings + mail.from = Setting.email_sender + mail.delivery_method.settings.merge!({ address: Setting.smtp_host, + port: Setting.smtp_port, + user_name: Setting.smtp_username, + password: Setting.smtp_password, + tls: ENV.fetch("SMTP_TLS_ENABLED", "true") == "true" }) + end end diff --git a/app/models/account/balance.rb b/app/models/account/balance.rb index 1f2ace10..cd361269 100644 --- a/app/models/account/balance.rb +++ b/app/models/account/balance.rb @@ -1,9 +1,9 @@ class Account::Balance < ApplicationRecord - include Monetizable + include Monetizable - belongs_to :account - validates :account, :date, :balance, presence: true - monetize :balance - scope :in_period, ->(period) { period.date_range.nil? ? all : where(date: period.date_range) } - scope :chronological, -> { order(:date) } + belongs_to :account + validates :account, :date, :balance, presence: true + monetize :balance + scope :in_period, ->(period) { period.date_range.nil? ? all : where(date: period.date_range) } + scope :chronological, -> { order(:date) } end diff --git a/app/models/category.rb b/app/models/category.rb index cfcab4ef..516b7631 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -47,7 +47,7 @@ class Category < ApplicationRecord private - def clear_internal_category - self.internal_category = nil - end + def clear_internal_category + self.internal_category = nil + end end diff --git a/app/models/concerns/monetizable.rb b/app/models/concerns/monetizable.rb index 3b363147..8179271b 100644 --- a/app/models/concerns/monetizable.rb +++ b/app/models/concerns/monetizable.rb @@ -1,14 +1,14 @@ module Monetizable - extend ActiveSupport::Concern + extend ActiveSupport::Concern - class_methods do - def monetize(*fields) - fields.each do |field| - define_method("#{field}_money") do - value = self.send(field) - value.nil? ? nil : Money.new(value, currency || Money.default_currency) - end + class_methods do + def monetize(*fields) + fields.each do |field| + define_method("#{field}_money") do + value = self.send(field) + value.nil? ? nil : Money.new(value, currency || Money.default_currency) end end end + end end diff --git a/app/models/invite_code.rb b/app/models/invite_code.rb index 59ebf855..f2cbcd7e 100644 --- a/app/models/invite_code.rb +++ b/app/models/invite_code.rb @@ -16,10 +16,10 @@ class InviteCode < ApplicationRecord private - def generate_token - loop do - self.token = SecureRandom.hex(4) - break token unless self.class.exists?(token: token) + def generate_token + loop do + self.token = SecureRandom.hex(4) + break token unless self.class.exists?(token: token) + end end - end end diff --git a/app/models/period.rb b/app/models/period.rb index 3a10f15e..4a8a3ffa 100644 --- a/app/models/period.rb +++ b/app/models/period.rb @@ -1,35 +1,35 @@ class Period - attr_reader :name, :date_range + attr_reader :name, :date_range - def self.find_by_name(name) - INDEX[name] - end - - def self.names - INDEX.keys.sort - end - - def initialize(name: "custom", date_range:) - @name = name - @date_range = date_range - end - - def extend_backward(duration) - Period.new(name: name + "_extended", date_range: (date_range.first - duration)..date_range.last) - end - - BUILTIN = [ - 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_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) - ] - - INDEX = BUILTIN.index_by(&:name) - - BUILTIN.each do |period| - define_singleton_method(period.name) do - period - end + def self.find_by_name(name) + INDEX[name] + end + + def self.names + INDEX.keys.sort + end + + def initialize(name: "custom", date_range:) + @name = name + @date_range = date_range + end + + def extend_backward(duration) + Period.new(name: name + "_extended", date_range: (date_range.first - duration)..date_range.last) + end + + BUILTIN = [ + 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_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) + ] + + INDEX = BUILTIN.index_by(&:name) + + BUILTIN.each do |period| + define_singleton_method(period.name) do + period end + end end diff --git a/app/models/provider/github.rb b/app/models/provider/github.rb index 4cb19cbf..570c558e 100644 --- a/app/models/provider/github.rb +++ b/app/models/provider/github.rb @@ -16,11 +16,11 @@ class Provider::Github latest_commit = Octokit.branch(repo, branch) release_info = if latest_release - { - version: latest_version, - url: latest_release.html_url, - commit_sha: Octokit.commit(repo, latest_release.tag_name).sha - } + { + version: latest_version, + url: latest_release.html_url, + commit_sha: Octokit.commit(repo, latest_release.tag_name).sha + } end commit_info = { diff --git a/app/models/user.rb b/app/models/user.rb index 62f5209c..a4a9dd04 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -74,18 +74,18 @@ class User < ApplicationRecord private - def last_user_in_family? - family.users.count == 1 - end - - def deactivated_email - email.gsub(/@/, "-deactivated-#{SecureRandom.uuid}@") - end - - def profile_image_size - if profile_image.attached? && profile_image.byte_size > 5.megabytes - # i18n-tasks-use t('activerecord.errors.models.user.attributes.profile_image.invalid_file_size') - errors.add(:profile_image, :invalid_file_size, max_megabytes: 5) + def last_user_in_family? + family.users.count == 1 + end + + def deactivated_email + email.gsub(/@/, "-deactivated-#{SecureRandom.uuid}@") + end + + def profile_image_size + if profile_image.attached? && profile_image.byte_size > 5.megabytes + # i18n-tasks-use t('activerecord.errors.models.user.attributes.profile_image.invalid_file_size') + errors.add(:profile_image, :invalid_file_size, max_megabytes: 5) + end end - end end diff --git a/app/models/value_group.rb b/app/models/value_group.rb index e9d2d5b8..147f7cac 100644 --- a/app/models/value_group.rb +++ b/app/models/value_group.rb @@ -3,100 +3,100 @@ attr_reader :name, :children, :value, :currency def initialize(name, currency = Money.default_currency) - @name = name - @currency = Money::Currency.new(currency) - @children = [] + @name = name + @currency = Money::Currency.new(currency) + @children = [] end def sum - return value if is_value_node? - return Money.new(0, currency) if children.empty? && value.nil? - children.sum(&:sum) + return value if is_value_node? + return Money.new(0, currency) if children.empty? && value.nil? + children.sum(&:sum) end def avg - return value if is_value_node? - return Money.new(0, currency) if children.empty? && value.nil? - leaf_values = value_nodes.map(&:value) - leaf_values.compact.sum / leaf_values.compact.size + return value if is_value_node? + return Money.new(0, currency) if children.empty? && value.nil? + leaf_values = value_nodes.map(&:value) + leaf_values.compact.sum / leaf_values.compact.size end 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| - child.series.values.each do |series_value| - acc[series_value.date] += series_value.value - end + summed_by_date = children.each_with_object(Hash.new(0)) do |child, acc| + child.series.values.each do |series_value| + acc[series_value.date] += series_value.value 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 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 contain money values in the node's currency" unless _series.values.all? { |v| v.value.currency == currency } - @series = _series + 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 } + @series = _series end def value_nodes - return [ self ] unless value.nil? - children.flat_map { |child| child.value_nodes } + return [ self ] unless value.nil? + children.flat_map { |child| child.value_nodes } end def empty? - value_nodes.empty? + value_nodes.empty? end 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 def add_child_group(name, currency = Money.default_currency) - raise "Cannot add subgroup to node with a value" if is_value_node? - child = self.class.new(name, currency) - child.parent = self - @children << child - child + raise "Cannot add subgroup to node with a value" if is_value_node? + child = self.class.new(name, currency) + child.parent = self + @children << child + child end def add_value_node(original, value, series = nil) - raise "Cannot add value node to a non-leaf node" unless can_add_value_node? - child = self.class.new(original.name) - child.original = original - child.value = value - child.series = series - child.parent = self - @children << child - child + raise "Cannot add value node to a non-leaf node" unless can_add_value_node? + child = self.class.new(original.name) + child.original = original + child.value = value + child.series = series + child.parent = self + @children << child + child end def value=(value) - 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) - @value = value - @currency = value.currency + 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) + @value = value + @currency = value.currency end def is_leaf_node? - children.empty? + children.empty? end def is_value_node? - value.present? + value.present? end private - def can_add_value_node? - return false if is_value_node? - children.empty? || children.all?(&:is_value_node?) - end + def can_add_value_node? + return false if is_value_node? + children.empty? || children.all?(&:is_value_node?) + end end diff --git a/lib/money/arithmetic.rb b/lib/money/arithmetic.rb index dc48abfd..99000066 100644 --- a/lib/money/arithmetic.rb +++ b/lib/money/arithmetic.rb @@ -1,62 +1,62 @@ module Money::Arithmetic - CoercedNumeric = Struct.new(:value) + CoercedNumeric = Struct.new(:value) - def +(other) - if other.is_a?(Money) - self.class.new(amount + other.amount, currency) - else - value = other.is_a?(CoercedNumeric) ? other.value : other - self.class.new(amount + value, currency) - end + def +(other) + if other.is_a?(Money) + self.class.new(amount + other.amount, currency) + else + value = other.is_a?(CoercedNumeric) ? other.value : other + self.class.new(amount + value, currency) end + end - def -(other) - if other.is_a?(Money) - self.class.new(amount - other.amount, currency) - else - value = other.is_a?(CoercedNumeric) ? other.value : other - self.class.new(amount - value, currency) - end + def -(other) + if other.is_a?(Money) + self.class.new(amount - other.amount, currency) + else + value = other.is_a?(CoercedNumeric) ? other.value : other + self.class.new(amount - value, currency) end + end - def -@ - self.class.new(-amount, currency) - end + def -@ + self.class.new(-amount, currency) + end - def *(other) - 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 - self.class.new(amount * value, currency) - end + def *(other) + 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 + self.class.new(amount * value, currency) + end - def /(other) - if other.is_a?(self.class) - amount / other.amount - else - raise TypeError, "can't divide Numeric by Money" if other.is_a?(CoercedNumeric) - self.class.new(amount / other, currency) - end + def /(other) + if other.is_a?(self.class) + amount / other.amount + else + raise TypeError, "can't divide Numeric by Money" if other.is_a?(CoercedNumeric) + self.class.new(amount / other, currency) end + end - def abs - self.class.new(amount.abs, currency) - end + def abs + self.class.new(amount.abs, currency) + end - def zero? - amount.zero? - end + def zero? + amount.zero? + end - def negative? - amount.negative? - end + def negative? + amount.negative? + end - def positive? - amount.positive? - end + def positive? + amount.positive? + end - # 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 - def coerce(other) - [ self, CoercedNumeric.new(other) ] - end + # 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 + def coerce(other) + [ self, CoercedNumeric.new(other) ] + end end diff --git a/lib/money/currency.rb b/lib/money/currency.rb index b35e2b3f..95e63bcf 100644 --- a/lib/money/currency.rb +++ b/lib/money/currency.rb @@ -1,69 +1,69 @@ 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 - @@instances = {} + # Cached instances by iso code + @@instances = {} - class << self - def new(object) - iso_code = case object - when String, Symbol - object.to_s.downcase - when Money::Currency - object.iso_code.downcase - else - raise ArgumentError, "Invalid argument type" - end - - @@instances[iso_code] ||= super(iso_code) + class << self + def new(object) + iso_code = case object + when String, Symbol + object.to_s.downcase + when Money::Currency + object.iso_code.downcase + else + raise ArgumentError, "Invalid argument type" end - def all - @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 + @@instances[iso_code] ||= super(iso_code) 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"] + def all + @all ||= YAML.load_file(CURRENCIES_FILE_PATH) end - def step - (1.0/10**default_precision) + def all_instances + all.values.map { |currency_data| new(currency_data["iso_code"]) } end - def <=>(other) - return nil unless other.is_a?(Money::Currency) - @iso_code <=> other.iso_code + def popular + all.values.sort_by { |currency| currency["priority"] }.first(12).map { |currency_data| new(currency_data["iso_code"]) } 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 diff --git a/test/lib/money/currency_test.rb b/test/lib/money/currency_test.rb index 3b94ccc6..bc5d9f7a 100644 --- a/test/lib/money/currency_test.rb +++ b/test/lib/money/currency_test.rb @@ -1,53 +1,53 @@ require "test_helper" class Money::CurrencyTest < ActiveSupport::TestCase - setup do - @currency = Money::Currency.new(:usd) - end + setup do + @currency = Money::Currency.new(:usd) + end - test "has many currencies" do - assert_operator Money::Currency.all.count, :>, 100 - end + test "has many currencies" do + assert_operator Money::Currency.all.count, :>, 100 + end - test "can test equality of currencies" do - assert_equal Money::Currency.new(:usd), Money::Currency.new(:usd) - assert_not_equal Money::Currency.new(:usd), Money::Currency.new(:eur) - end + test "can test equality of currencies" do + assert_equal Money::Currency.new(:usd), Money::Currency.new(:usd) + assert_not_equal Money::Currency.new(:usd), Money::Currency.new(:eur) + end - test "can get metadata about a currency" do - assert_equal "USD", @currency.iso_code - assert_equal "United States Dollar", @currency.name - assert_equal "$", @currency.symbol - assert_equal 1, @currency.priority - assert_equal "Cent", @currency.minor_unit - assert_equal 100, @currency.minor_unit_conversion - assert_equal 1, @currency.smallest_denomination - assert_equal ".", @currency.separator - assert_equal ",", @currency.delimiter - assert_equal "%u%n", @currency.default_format - assert_equal 2, @currency.default_precision - end + test "can get metadata about a currency" do + assert_equal "USD", @currency.iso_code + assert_equal "United States Dollar", @currency.name + assert_equal "$", @currency.symbol + assert_equal 1, @currency.priority + assert_equal "Cent", @currency.minor_unit + assert_equal 100, @currency.minor_unit_conversion + assert_equal 1, @currency.smallest_denomination + assert_equal ".", @currency.separator + assert_equal ",", @currency.delimiter + assert_equal "%u%n", @currency.default_format + assert_equal 2, @currency.default_precision + end - test "can extract cents string from amount" do - value1 = Money.new(100) - value2 = Money.new(100.1) - value3 = Money.new(100.12) - value4 = Money.new(100.123) - value5 = Money.new(200, :jpy) + test "can extract cents string from amount" do + value1 = Money.new(100) + value2 = Money.new(100.1) + value3 = Money.new(100.12) + value4 = Money.new(100.123) + value5 = Money.new(200, :jpy) - assert_equal "00", value1.cents_str - assert_equal "10", value2.cents_str - assert_equal "12", value3.cents_str - assert_equal "12", value4.cents_str - assert_equal "", value5.cents_str + assert_equal "00", value1.cents_str + assert_equal "10", value2.cents_str + assert_equal "12", value3.cents_str + assert_equal "12", value4.cents_str + assert_equal "", value5.cents_str - assert_equal "", value4.cents_str(0) - assert_equal "1", value4.cents_str(1) - assert_equal "12", value4.cents_str(2) - assert_equal "123", value4.cents_str(3) - end + assert_equal "", value4.cents_str(0) + assert_equal "1", value4.cents_str(1) + assert_equal "12", value4.cents_str(2) + assert_equal "123", value4.cents_str(3) + end - test "step returns the smallest value of the currency" do - assert_equal 0.01, @currency.step - end + test "step returns the smallest value of the currency" do + assert_equal 0.01, @currency.step + end end diff --git a/test/models/value_group_test.rb b/test/models/value_group_test.rb index e27010a1..5c9e2bbf 100644 --- a/test/models/value_group_test.rb +++ b/test/models/value_group_test.rb @@ -1,141 +1,141 @@ require "test_helper" require "ostruct" class ValueGroupTest < ActiveSupport::TestCase - setup do - # Level 1 - @assets = ValueGroup.new("Assets", :usd) + setup do + # Level 1 + @assets = ValueGroup.new("Assets", :usd) - # Level 2 - @depositories = @assets.add_child_group("Depositories", :usd) - @other_assets = @assets.add_child_group("Other Assets", :usd) + # Level 2 + @depositories = @assets.add_child_group("Depositories", :usd) + @other_assets = @assets.add_child_group("Other Assets", :usd) - # Level 3 (leaf/value nodes) - @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)) - @collectable_node = @other_assets.add_value_node(OpenStruct.new({ name: "Collectable", value: Money.new(550) }), Money.new(550)) + # Level 3 (leaf/value nodes) + @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)) + @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 - 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 - - assert_raises(RuntimeError) do - grandparent.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