mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-18 20:59:39 +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:
|
||||||
inherit_gem: { rubocop-rails-omakase: rubocop.yml }
|
rubocop-rails-omakase: rubocop.yml
|
||||||
|
|
||||||
# Overwrite or add rules to create your own house style
|
Layout/IndentationWidth:
|
||||||
#
|
Enabled: true
|
||||||
# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]`
|
|
||||||
# Layout/SpaceInsideArrayLiteralBrackets:
|
Layout/IndentationStyle:
|
||||||
# Enabled: false
|
EnforcedStyle: spaces
|
||||||
Layout/ElseAlignment:
|
IndentationWidth: 2
|
||||||
Enabled: false
|
|
||||||
Layout/EndAlignment:
|
Layout/IndentationConsistency:
|
||||||
Enabled: false
|
Enabled: true
|
||||||
|
|
||||||
|
Layout/SpaceInsidePercentLiteralDelimiters:
|
||||||
|
Enabled: true
|
4
Gemfile
4
Gemfile
|
@ -42,12 +42,12 @@ gem "inline_svg"
|
||||||
gem "octokit"
|
gem "octokit"
|
||||||
gem "pagy"
|
gem "pagy"
|
||||||
gem "rails-settings-cached"
|
gem "rails-settings-cached"
|
||||||
gem "tzinfo-data", platforms: %i[ windows jruby ]
|
gem "tzinfo-data", platforms: %i[windows jruby]
|
||||||
gem "csv"
|
gem "csv"
|
||||||
gem "redcarpet"
|
gem "redcarpet"
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem "debug", platforms: %i[ mri windows ]
|
gem "debug", platforms: %i[mri windows]
|
||||||
gem "brakeman", require: false
|
gem "brakeman", require: false
|
||||||
gem "rubocop-rails-omakase", require: false
|
gem "rubocop-rails-omakase", require: false
|
||||||
gem "i18n-tasks"
|
gem "i18n-tasks"
|
||||||
|
|
|
@ -2,7 +2,7 @@ class Account::EntriesController < ApplicationController
|
||||||
layout :with_sidebar
|
layout :with_sidebar
|
||||||
|
|
||||||
before_action :set_account
|
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
|
def edit
|
||||||
render entryable_view_path(:edit)
|
render entryable_view_path(:edit)
|
||||||
|
|
|
@ -8,7 +8,7 @@ class Account::TradesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def index
|
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
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|
|
@ -2,7 +2,7 @@ class AccountsController < ApplicationController
|
||||||
layout :with_sidebar
|
layout :with_sidebar
|
||||||
|
|
||||||
include Filterable
|
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
|
def index
|
||||||
@institutions = Current.family.institutions
|
@institutions = Current.family.institutions
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
class CategoriesController < ApplicationController
|
class CategoriesController < ApplicationController
|
||||||
layout :with_sidebar
|
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
|
before_action :set_transaction, only: :create
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
require "ostruct"
|
require "ostruct"
|
||||||
|
|
||||||
class ImportsController < ApplicationController
|
class ImportsController < ApplicationController
|
||||||
before_action :set_import, except: %i[ index new create ]
|
before_action :set_import, except: %i[index new create]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@imports = Current.family.imports
|
@imports = Current.family.imports
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
class InstitutionsController < ApplicationController
|
class InstitutionsController < ApplicationController
|
||||||
before_action :set_institution, except: %i[ new create ]
|
before_action :set_institution, except: %i[new create]
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@institution = Institution.new
|
@institution = Institution.new
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
class MerchantsController < ApplicationController
|
class MerchantsController < ApplicationController
|
||||||
layout :with_sidebar
|
layout :with_sidebar
|
||||||
|
|
||||||
before_action :set_merchant, only: %i[ edit update destroy ]
|
before_action :set_merchant, only: %i[edit update destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@merchants = Current.family.merchants.alphabetically
|
@merchants = Current.family.merchants.alphabetically
|
||||||
|
@ -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
|
||||||
|
|
|
@ -3,7 +3,7 @@ class PasswordResetsController < ApplicationController
|
||||||
|
|
||||||
layout "auth"
|
layout "auth"
|
||||||
|
|
||||||
before_action :set_user_by_token, only: %i[ edit update ]
|
before_action :set_user_by_token, only: %i[edit update]
|
||||||
|
|
||||||
def new
|
def new
|
||||||
end
|
end
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
class TagsController < ApplicationController
|
class TagsController < ApplicationController
|
||||||
layout :with_sidebar
|
layout :with_sidebar
|
||||||
|
|
||||||
before_action :set_tag, only: %i[ edit update ]
|
before_action :set_tag, only: %i[edit update]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@tags = Current.family.tags.alphabetically
|
@tags = Current.family.tags.alphabetically
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
class Account::EntryBuilder
|
class Account::EntryBuilder
|
||||||
include ActiveModel::Model
|
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
|
attr_accessor :type, :date, :qty, :ticker, :price, :amount, :currency, :account, :transfer_account_id
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
module Account::Entryable
|
module Account::Entryable
|
||||||
extend ActiveSupport::Concern
|
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)
|
def self.from_type(entryable_type)
|
||||||
entryable_type.presence_in(TYPES).constantize
|
entryable_type.presence_in(TYPES).constantize
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
class Account::TradeBuilder < Account::EntryBuilder
|
class Account::TradeBuilder < Account::EntryBuilder
|
||||||
include ActiveModel::Model
|
include ActiveModel::Model
|
||||||
|
|
||||||
TYPES = %w[ buy sell ].freeze
|
TYPES = %w[buy sell].freeze
|
||||||
|
|
||||||
attr_accessor :type, :qty, :price, :ticker, :date, :account
|
attr_accessor :type, :qty, :price, :ticker, :date, :account
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ class Account::Transaction < ApplicationRecord
|
||||||
private
|
private
|
||||||
|
|
||||||
def searchable_keys
|
def searchable_keys
|
||||||
%i[ categories merchants ]
|
%i[categories merchants]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
class Account::TransactionBuilder
|
class Account::TransactionBuilder
|
||||||
include ActiveModel::Model
|
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
|
attr_accessor :type, :amount, :date, :account, :transfer_account_id
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
module Accountable
|
module Accountable
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
ASSET_TYPES = %w[ Depository Investment Crypto Property Vehicle OtherAsset ]
|
ASSET_TYPES = %w[Depository Investment Crypto Property Vehicle OtherAsset]
|
||||||
LIABILITY_TYPES = %w[ CreditCard Loan OtherLiability ]
|
LIABILITY_TYPES = %w[CreditCard Loan OtherLiability]
|
||||||
TYPES = ASSET_TYPES + LIABILITY_TYPES
|
TYPES = ASSET_TYPES + LIABILITY_TYPES
|
||||||
|
|
||||||
def self.from_type(type)
|
def self.from_type(type)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
class TimeSeries
|
class TimeSeries
|
||||||
DIRECTIONS = %w[ up down ].freeze
|
DIRECTIONS = %w[up down].freeze
|
||||||
|
|
||||||
attr_reader :values, :favorable_direction
|
attr_reader :values, :favorable_direction
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -63,5 +63,5 @@ Rails.application.configure do
|
||||||
# Raise error when a before_action's only/except options reference missing actions
|
# Raise error when a before_action's only/except options reference missing actions
|
||||||
config.action_controller.raise_on_missing_callback_actions = true
|
config.action_controller.raise_on_missing_callback_actions = true
|
||||||
|
|
||||||
config.autoload_paths += %w[ test/support ]
|
config.autoload_paths += %w[test/support]
|
||||||
end
|
end
|
||||||
|
|
|
@ -42,8 +42,8 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :tags, except: %i[ show destroy ] do
|
resources :tags, except: %i[show destroy] do
|
||||||
resources :deletions, only: %i[ new create ], module: :tag
|
resources :deletions, only: %i[new create], module: :tag
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :category do
|
namespace :category do
|
||||||
|
@ -51,16 +51,16 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :categories do
|
resources :categories do
|
||||||
resources :deletions, only: %i[ new create ], module: :category
|
resources :deletions, only: %i[new create], module: :category
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :merchants, only: %i[ index new create edit update destroy ]
|
resources :merchants, only: %i[index new create edit update destroy]
|
||||||
|
|
||||||
namespace :account do
|
namespace :account do
|
||||||
resources :transfers, only: %i[ new create destroy ]
|
resources :transfers, only: %i[new create destroy]
|
||||||
|
|
||||||
namespace :transaction do
|
namespace :transaction do
|
||||||
resources :rules, only: %i[ index ]
|
resources :rules, only: %i[index]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -78,21 +78,21 @@ Rails.application.routes.draw do
|
||||||
scope module: :account do
|
scope module: :account do
|
||||||
resource :logo, only: :show
|
resource :logo, only: :show
|
||||||
|
|
||||||
resources :holdings, only: %i[ index new show ]
|
resources :holdings, only: %i[index new show]
|
||||||
resources :cashes, only: :index
|
resources :cashes, only: :index
|
||||||
|
|
||||||
resources :transactions, only: %i[ index update ]
|
resources :transactions, only: %i[index update]
|
||||||
resources :valuations, only: %i[ index new create ]
|
resources :valuations, only: %i[index new create]
|
||||||
resources :trades, 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
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :properties, only: %i[ create update ]
|
resources :properties, only: %i[create update]
|
||||||
resources :vehicles, 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
|
collection do
|
||||||
post "bulk_delete"
|
post "bulk_delete"
|
||||||
get "bulk_edit"
|
get "bulk_edit"
|
||||||
|
@ -103,7 +103,7 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :institutions, except: %i[ index show ]
|
resources :institutions, except: %i[index show]
|
||||||
|
|
||||||
resources :issues, only: :show
|
resources :issues, only: :show
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
end
|
||||||
|
|
||||||
@@instances[iso_code] ||= super(iso_code)
|
@@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
|
|
||||||
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -39,7 +39,7 @@ class Import::CsvTest < ActiveSupport::TestCase
|
||||||
test "CSV with semicolon column separator" do
|
test "CSV with semicolon column separator" do
|
||||||
csv = Import::Csv.new(valid_csv_str_with_semicolon_separator, col_sep: ";")
|
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 4, csv.table.size
|
||||||
assert_equal "Paycheck", csv.table[3][1]
|
assert_equal "Paycheck", csv.table[3][1]
|
||||||
end
|
end
|
||||||
|
@ -84,7 +84,7 @@ class Import::CsvTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
csv = Import::Csv.create_with_field_mappings(raw_file_str, fields, mappings)
|
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 2, csv.table.size
|
||||||
assert_equal "Amazon stuff", csv.table[1][1]
|
assert_equal "Amazon stuff", csv.table[1][1]
|
||||||
end
|
end
|
||||||
|
@ -113,7 +113,7 @@ class Import::CsvTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
csv = Import::Csv.create_with_field_mappings(raw_file_str, fields, mappings, ";")
|
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 2, csv.table.size
|
||||||
assert_equal "Amazon stuff", csv.table[1][1]
|
assert_equal "Amazon stuff", csv.table[1][1]
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue