mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-18 12:49:38 +02:00
Rubocop updates (#1118)
* Minimal code style enforcement * Formatting and lint code updates (no change in functionality)
This commit is contained in:
parent
359bceb58e
commit
eef4c2643b
41 changed files with 529 additions and 519 deletions
7
.editorconfig
Normal file
7
.editorconfig
Normal file
|
@ -0,0 +1,7 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
25
.rubocop.yml
25
.rubocop.yml
|
@ -1,12 +1,15 @@
|
|||
# Omakase Ruby styling for Rails
|
||||
inherit_gem: { rubocop-rails-omakase: rubocop.yml }
|
||||
inherit_gem:
|
||||
rubocop-rails-omakase: rubocop.yml
|
||||
|
||||
Layout/IndentationWidth:
|
||||
Enabled: true
|
||||
|
||||
# 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
|
||||
Layout/IndentationStyle:
|
||||
EnforcedStyle: spaces
|
||||
IndentationWidth: 2
|
||||
|
||||
Layout/IndentationConsistency:
|
||||
Enabled: true
|
||||
|
||||
Layout/SpaceInsidePercentLiteralDelimiters:
|
||||
Enabled: true
|
4
Gemfile
4
Gemfile
|
@ -42,12 +42,12 @@ gem "inline_svg"
|
|||
gem "octokit"
|
||||
gem "pagy"
|
||||
gem "rails-settings-cached"
|
||||
gem "tzinfo-data", platforms: %i[ windows jruby ]
|
||||
gem "tzinfo-data", platforms: %i[windows jruby]
|
||||
gem "csv"
|
||||
gem "redcarpet"
|
||||
|
||||
group :development, :test do
|
||||
gem "debug", platforms: %i[ mri windows ]
|
||||
gem "debug", platforms: %i[mri windows]
|
||||
gem "brakeman", require: false
|
||||
gem "rubocop-rails-omakase", require: false
|
||||
gem "i18n-tasks"
|
||||
|
|
|
@ -2,7 +2,7 @@ class Account::EntriesController < ApplicationController
|
|||
layout :with_sidebar
|
||||
|
||||
before_action :set_account
|
||||
before_action :set_entry, only: %i[ edit update show destroy ]
|
||||
before_action :set_entry, only: %i[edit update show destroy]
|
||||
|
||||
def edit
|
||||
render entryable_view_path(:edit)
|
||||
|
|
|
@ -8,7 +8,7 @@ class Account::TradesController < ApplicationController
|
|||
end
|
||||
|
||||
def index
|
||||
@entries = @account.entries.reverse_chronological.where(entryable_type: %w[ Account::Trade Account::Transaction ])
|
||||
@entries = @account.entries.reverse_chronological.where(entryable_type: %w[Account::Trade Account::Transaction])
|
||||
end
|
||||
|
||||
def create
|
||||
|
|
|
@ -2,7 +2,7 @@ class AccountsController < ApplicationController
|
|||
layout :with_sidebar
|
||||
|
||||
include Filterable
|
||||
before_action :set_account, only: %i[ edit show destroy sync update ]
|
||||
before_action :set_account, only: %i[edit show destroy sync update]
|
||||
|
||||
def index
|
||||
@institutions = Current.family.institutions
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class CategoriesController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
before_action :set_category, only: %i[ edit update ]
|
||||
before_action :set_category, only: %i[edit update]
|
||||
before_action :set_transaction, only: :create
|
||||
|
||||
def index
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
require "ostruct"
|
||||
|
||||
class ImportsController < ApplicationController
|
||||
before_action :set_import, except: %i[ index new create ]
|
||||
before_action :set_import, except: %i[index new create]
|
||||
|
||||
def index
|
||||
@imports = Current.family.imports
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class InstitutionsController < ApplicationController
|
||||
before_action :set_institution, except: %i[ new create ]
|
||||
before_action :set_institution, except: %i[new create]
|
||||
|
||||
def new
|
||||
@institution = Institution.new
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class MerchantsController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
before_action :set_merchant, only: %i[ edit update destroy ]
|
||||
before_action :set_merchant, only: %i[edit update destroy]
|
||||
|
||||
def index
|
||||
@merchants = Current.family.merchants.alphabetically
|
||||
|
@ -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
|
||||
|
|
|
@ -3,7 +3,7 @@ class PasswordResetsController < ApplicationController
|
|||
|
||||
layout "auth"
|
||||
|
||||
before_action :set_user_by_token, only: %i[ edit update ]
|
||||
before_action :set_user_by_token, only: %i[edit update]
|
||||
|
||||
def new
|
||||
end
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class TagsController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
before_action :set_tag, only: %i[ edit update ]
|
||||
before_action :set_tag, only: %i[edit update]
|
||||
|
||||
def index
|
||||
@tags = Current.family.tags.alphabetically
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class Account::EntryBuilder
|
||||
include ActiveModel::Model
|
||||
|
||||
TYPES = %w[ income expense buy sell interest transfer_in transfer_out ].freeze
|
||||
TYPES = %w[income expense buy sell interest transfer_in transfer_out].freeze
|
||||
|
||||
attr_accessor :type, :date, :qty, :ticker, :price, :amount, :currency, :account, :transfer_account_id
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module Account::Entryable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
TYPES = %w[ Account::Valuation Account::Transaction Account::Trade ]
|
||||
TYPES = %w[Account::Valuation Account::Transaction Account::Trade]
|
||||
|
||||
def self.from_type(entryable_type)
|
||||
entryable_type.presence_in(TYPES).constantize
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class Account::TradeBuilder < Account::EntryBuilder
|
||||
include ActiveModel::Model
|
||||
|
||||
TYPES = %w[ buy sell ].freeze
|
||||
TYPES = %w[buy sell].freeze
|
||||
|
||||
attr_accessor :type, :qty, :price, :ticker, :date, :account
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ class Account::Transaction < ApplicationRecord
|
|||
private
|
||||
|
||||
def searchable_keys
|
||||
%i[ categories merchants ]
|
||||
%i[categories merchants]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class Account::TransactionBuilder
|
||||
include ActiveModel::Model
|
||||
|
||||
TYPES = %w[ income expense interest transfer_in transfer_out ].freeze
|
||||
TYPES = %w[income expense interest transfer_in transfer_out].freeze
|
||||
|
||||
attr_accessor :type, :amount, :date, :account, :transfer_account_id
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
module Accountable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
ASSET_TYPES = %w[ Depository Investment Crypto Property Vehicle OtherAsset ]
|
||||
LIABILITY_TYPES = %w[ CreditCard Loan OtherLiability ]
|
||||
ASSET_TYPES = %w[Depository Investment Crypto Property Vehicle OtherAsset]
|
||||
LIABILITY_TYPES = %w[CreditCard Loan OtherLiability]
|
||||
TYPES = ASSET_TYPES + LIABILITY_TYPES
|
||||
|
||||
def self.from_type(type)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class TimeSeries
|
||||
DIRECTIONS = %w[ up down ].freeze
|
||||
DIRECTIONS = %w[up down].freeze
|
||||
|
||||
attr_reader :values, :favorable_direction
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -63,5 +63,5 @@ Rails.application.configure do
|
|||
# Raise error when a before_action's only/except options reference missing actions
|
||||
config.action_controller.raise_on_missing_callback_actions = true
|
||||
|
||||
config.autoload_paths += %w[ test/support ]
|
||||
config.autoload_paths += %w[test/support]
|
||||
end
|
||||
|
|
|
@ -42,8 +42,8 @@ Rails.application.routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
resources :tags, except: %i[ show destroy ] do
|
||||
resources :deletions, only: %i[ new create ], module: :tag
|
||||
resources :tags, except: %i[show destroy] do
|
||||
resources :deletions, only: %i[new create], module: :tag
|
||||
end
|
||||
|
||||
namespace :category do
|
||||
|
@ -51,16 +51,16 @@ Rails.application.routes.draw do
|
|||
end
|
||||
|
||||
resources :categories do
|
||||
resources :deletions, only: %i[ new create ], module: :category
|
||||
resources :deletions, only: %i[new create], module: :category
|
||||
end
|
||||
|
||||
resources :merchants, only: %i[ index new create edit update destroy ]
|
||||
resources :merchants, only: %i[index new create edit update destroy]
|
||||
|
||||
namespace :account do
|
||||
resources :transfers, only: %i[ new create destroy ]
|
||||
resources :transfers, only: %i[new create destroy]
|
||||
|
||||
namespace :transaction do
|
||||
resources :rules, only: %i[ index ]
|
||||
resources :rules, only: %i[index]
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -78,21 +78,21 @@ Rails.application.routes.draw do
|
|||
scope module: :account do
|
||||
resource :logo, only: :show
|
||||
|
||||
resources :holdings, only: %i[ index new show ]
|
||||
resources :holdings, only: %i[index new show]
|
||||
resources :cashes, only: :index
|
||||
|
||||
resources :transactions, only: %i[ index update ]
|
||||
resources :valuations, only: %i[ index new create ]
|
||||
resources :trades, only: %i[ index new create ]
|
||||
resources :transactions, only: %i[index update]
|
||||
resources :valuations, only: %i[index new create]
|
||||
resources :trades, only: %i[index new create]
|
||||
|
||||
resources :entries, only: %i[ edit update show destroy ]
|
||||
resources :entries, only: %i[edit update show destroy]
|
||||
end
|
||||
end
|
||||
|
||||
resources :properties, only: %i[ create update ]
|
||||
resources :vehicles, only: %i[ create update ]
|
||||
resources :properties, only: %i[create update]
|
||||
resources :vehicles, only: %i[create update]
|
||||
|
||||
resources :transactions, only: %i[ index new create ] do
|
||||
resources :transactions, only: %i[index new create] do
|
||||
collection do
|
||||
post "bulk_delete"
|
||||
get "bulk_edit"
|
||||
|
@ -103,7 +103,7 @@ Rails.application.routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
resources :institutions, except: %i[ index show ]
|
||||
resources :institutions, except: %i[index show]
|
||||
|
||||
resources :issues, only: :show
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
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)
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -39,7 +39,7 @@ class Import::CsvTest < ActiveSupport::TestCase
|
|||
test "CSV with semicolon column separator" do
|
||||
csv = Import::Csv.new(valid_csv_str_with_semicolon_separator, col_sep: ";")
|
||||
|
||||
assert_equal %w[ date name category tags amount ], csv.table.headers
|
||||
assert_equal %w[date name category tags amount], csv.table.headers
|
||||
assert_equal 4, csv.table.size
|
||||
assert_equal "Paycheck", csv.table[3][1]
|
||||
end
|
||||
|
@ -84,7 +84,7 @@ class Import::CsvTest < ActiveSupport::TestCase
|
|||
|
||||
csv = Import::Csv.create_with_field_mappings(raw_file_str, fields, mappings)
|
||||
|
||||
assert_equal %w[ date name ], csv.table.headers
|
||||
assert_equal %w[date name], csv.table.headers
|
||||
assert_equal 2, csv.table.size
|
||||
assert_equal "Amazon stuff", csv.table[1][1]
|
||||
end
|
||||
|
@ -113,7 +113,7 @@ class Import::CsvTest < ActiveSupport::TestCase
|
|||
|
||||
csv = Import::Csv.create_with_field_mappings(raw_file_str, fields, mappings, ";")
|
||||
|
||||
assert_equal %w[ date name ], csv.table.headers
|
||||
assert_equal %w[date name], csv.table.headers
|
||||
assert_equal 2, csv.table.size
|
||||
assert_equal "Amazon stuff", csv.table[1][1]
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue