generalis 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +68 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +46 -0
- data/.ruby-version +1 -0
- data/.vscode/settings.json +5 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +134 -0
- data/README.md +622 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/setup +8 -0
- data/generalis.gemspec +34 -0
- data/lib/generalis/account.rb +98 -0
- data/lib/generalis/accountable.rb +43 -0
- data/lib/generalis/asset.rb +7 -0
- data/lib/generalis/config.rb +15 -0
- data/lib/generalis/credit.rb +7 -0
- data/lib/generalis/debit.rb +7 -0
- data/lib/generalis/entry.rb +66 -0
- data/lib/generalis/expense.rb +7 -0
- data/lib/generalis/liability.rb +7 -0
- data/lib/generalis/link.rb +10 -0
- data/lib/generalis/linkable.rb +15 -0
- data/lib/generalis/revenue.rb +7 -0
- data/lib/generalis/rspec.rb +6 -0
- data/lib/generalis/transaction/double_entry.rb +37 -0
- data/lib/generalis/transaction/dsl.rb +70 -0
- data/lib/generalis/transaction/links.rb +29 -0
- data/lib/generalis/transaction/preparation.rb +45 -0
- data/lib/generalis/transaction.rb +109 -0
- data/lib/generalis/version.rb +5 -0
- data/lib/generalis.rb +55 -0
- data/lib/generators/factory_bot/templates/transactions.rb.erb +7 -0
- data/lib/generators/factory_bot/transaction_generator.rb +39 -0
- data/lib/generators/generalis/install_generator.rb +19 -0
- data/lib/generators/generalis/migrations_generator.rb +45 -0
- data/lib/generators/generalis/templates/base_transaction.rb +12 -0
- data/lib/generators/generalis/templates/create_ledger_accounts.rb.erb +18 -0
- data/lib/generators/generalis/templates/create_ledger_entries.rb.erb +25 -0
- data/lib/generators/generalis/templates/create_ledger_links.rb.erb +14 -0
- data/lib/generators/generalis/templates/create_ledger_transactions.rb.erb +14 -0
- data/lib/generators/generalis/templates/generalis.rb +6 -0
- data/lib/generators/generalis/templates/transaction.rb.erb +26 -0
- data/lib/generators/generalis/transaction_generator.rb +33 -0
- data/lib/generators/rspec/templates/transaction_spec.rb.erb +11 -0
- data/lib/generators/rspec/transaction_generator.rb +41 -0
- data/lib/rspec/change_balance_of_matcher.rb +32 -0
- data/lib/rspec/credit_account_matcher.rb +45 -0
- data/lib/rspec/debit_account_matcher.rb +45 -0
- data/lib/rspec/have_balance_matcher.rb +27 -0
- data/lib/rspec/helpers/format_helper.rb +28 -0
- data/lib/rspec/helpers/resolve_account_helper.rb +21 -0
- data/lib/rspec/helpers/resolve_amount_helper.rb +21 -0
- metadata +136 -0
data/lib/generalis.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
require 'active_support/core_ext'
|
5
|
+
|
6
|
+
require_relative 'generalis/config'
|
7
|
+
require_relative 'generalis/version'
|
8
|
+
|
9
|
+
module Generalis
|
10
|
+
autoload :Account, 'generalis/account'
|
11
|
+
autoload :Accountable, 'generalis/accountable'
|
12
|
+
autoload :Asset, 'generalis/asset'
|
13
|
+
autoload :Expense, 'generalis/expense'
|
14
|
+
autoload :Liability, 'generalis/liability'
|
15
|
+
autoload :Revenue, 'generalis/revenue'
|
16
|
+
|
17
|
+
autoload :Transaction, 'generalis/transaction'
|
18
|
+
autoload :Link, 'generalis/link'
|
19
|
+
autoload :Linkable, 'generalis/linkable'
|
20
|
+
|
21
|
+
autoload :Entry, 'generalis/entry'
|
22
|
+
autoload :Credit, 'generalis/credit'
|
23
|
+
autoload :Debit, 'generalis/debit'
|
24
|
+
|
25
|
+
# @return [Hash{String => Integer}]
|
26
|
+
def self.trial_balances
|
27
|
+
subquery = Entry
|
28
|
+
.group(:account_id, :currency)
|
29
|
+
.select(Entry.arel_table[:id].maximum)
|
30
|
+
|
31
|
+
Entry
|
32
|
+
.joins(:account)
|
33
|
+
.where(id: subquery)
|
34
|
+
.group(:currency)
|
35
|
+
.sum((Entry.arel_table[:balance_after_cents] * Account.arel_table[:coefficient]))
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Config]
|
39
|
+
def self.config
|
40
|
+
@config ||= Config.new.freeze
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [void]
|
44
|
+
def self.configure
|
45
|
+
config = Config.new
|
46
|
+
yield(config)
|
47
|
+
|
48
|
+
@config = config.freeze
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [String]
|
52
|
+
def self.table_name_prefix
|
53
|
+
config.table_name_prefix
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
|
5
|
+
module FactoryBot
|
6
|
+
module Generators
|
7
|
+
class TransactionGenerator < Rails::Generators::NamedBase
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
9
|
+
|
10
|
+
def create_transaction_factory
|
11
|
+
template 'transactions.rb.erb', "spec/factories/#{module_path}/#{file_name}.rb"
|
12
|
+
end
|
13
|
+
|
14
|
+
def file_name
|
15
|
+
class_name.underscore.pluralize
|
16
|
+
end
|
17
|
+
|
18
|
+
def qualified_class_name
|
19
|
+
"#{module_name}::#{class_name}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def class_name
|
23
|
+
"#{name.to_s.classify.chomp('Transaction')}Transaction"
|
24
|
+
end
|
25
|
+
|
26
|
+
def module_name
|
27
|
+
module_path.classify
|
28
|
+
end
|
29
|
+
|
30
|
+
def module_path
|
31
|
+
'ledger'
|
32
|
+
end
|
33
|
+
|
34
|
+
def factory_name
|
35
|
+
class_name.underscore
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
|
5
|
+
module Generalis
|
6
|
+
module Generators
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
9
|
+
|
10
|
+
def create_initializer
|
11
|
+
template 'generalis.rb', 'config/initializers/generalis.rb'
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_base_transaction
|
15
|
+
template 'base_transaction.rb', 'app/models/ledger/base_transaction.rb'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
require 'rails/generators/active_record'
|
5
|
+
|
6
|
+
module Generalis
|
7
|
+
module Generators
|
8
|
+
class MigrationsGenerator < Rails::Generators::Base
|
9
|
+
include Rails::Generators::Migration
|
10
|
+
|
11
|
+
source_root File.expand_path('templates', __dir__)
|
12
|
+
|
13
|
+
def self.next_migration_number(dir)
|
14
|
+
::ActiveRecord::Generators::Base.next_migration_number(dir)
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_migration_files
|
18
|
+
migration_template 'create_ledger_accounts.rb.erb', 'db/migrate/create_ledger_accounts.rb'
|
19
|
+
migration_template 'create_ledger_transactions.rb.erb', 'db/migrate/create_ledger_transactions.rb'
|
20
|
+
migration_template 'create_ledger_entries.rb.erb', 'db/migrate/create_ledger_entries.rb'
|
21
|
+
migration_template 'create_ledger_links.rb.erb', 'db/migrate/create_ledger_links.rb'
|
22
|
+
end
|
23
|
+
|
24
|
+
def json_column_type
|
25
|
+
case ActiveRecord::Base.connection.adapter_name
|
26
|
+
when 'SQLite' then ':string'
|
27
|
+
when 'MySQL' then ':json'
|
28
|
+
when 'PostgreSQL' then ':jsonb'
|
29
|
+
else
|
30
|
+
Rails.logger.warn('Unsupported database adapter; using String for JSON data types')
|
31
|
+
|
32
|
+
':string'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# @param name [Symbol, String]
|
39
|
+
# @return [String]
|
40
|
+
def prefixed_table_name(name)
|
41
|
+
":#{Generalis.table_name_prefix}#{name}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateLedgerAccounts < ActiveRecord::Migration[6.1]
|
4
|
+
def change
|
5
|
+
create_table <%= prefixed_table_name(:accounts) %> do |t|
|
6
|
+
t.string :type, null: false
|
7
|
+
t.belongs_to :owner, null: true, polymorphic: true
|
8
|
+
t.string :name, null: false
|
9
|
+
t.integer :coefficient, null: false
|
10
|
+
t.timestamps default: -> { 'NOW()' }
|
11
|
+
|
12
|
+
t.check_constraint 'coefficient IN (-1, +1)'
|
13
|
+
|
14
|
+
t.index %i[owner_type owner_id name], unique: true
|
15
|
+
t.index :name, unique: true, where: 'owner_id IS NULL'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateLedgerEntries < ActiveRecord::Migration[6.1]
|
4
|
+
def change
|
5
|
+
create_table <%= prefixed_table_name(:entries) %> do |t|
|
6
|
+
t.string :type, null: false
|
7
|
+
t.belongs_to :account, null: false, foreign_key: { to_table: <%= prefixed_table_name(:accounts) %> }
|
8
|
+
t.belongs_to :transaction, null: false, foreign_key: { to_table: <%= prefixed_table_name(:transactions) %> }
|
9
|
+
t.uuid :pair_id, null: true
|
10
|
+
t.string :currency, null: false
|
11
|
+
t.integer :amount_cents, null: false
|
12
|
+
t.integer :balance_after_cents, null: false
|
13
|
+
t.integer :coefficient, null: false
|
14
|
+
t.column :metadata, <%= json_column_type %>
|
15
|
+
t.timestamps default: -> { 'NOW()' }
|
16
|
+
|
17
|
+
t.check_constraint 'amount_cents >= 0'
|
18
|
+
t.check_constraint 'coefficient IN (-1, +1)'
|
19
|
+
|
20
|
+
t.index %i[transaction_id pair_id]
|
21
|
+
# Index for efficiently selecting the latest balance on the account.
|
22
|
+
t.index %i[account_id currency id], order: { id: :desc }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateLedgerLinks < ActiveRecord::Migration[6.1]
|
4
|
+
def change
|
5
|
+
create_table <%= prefixed_table_name(:links) %> do |t|
|
6
|
+
t.belongs_to :transaction, null: false, foreign_key: { on_delete: :cascade, to_table: <%= prefixed_table_name(:transactions) %> }
|
7
|
+
t.belongs_to :linkable, polymorphic: true, null: false
|
8
|
+
t.string :name, null: false
|
9
|
+
t.timestamps default: -> { 'NOW()' }
|
10
|
+
|
11
|
+
t.index %i[transaction_id name]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateLedgerTransactions < ActiveRecord::Migration[6.1]
|
4
|
+
def change
|
5
|
+
create_table <%= prefixed_table_name(:transactions) %> do |t|
|
6
|
+
t.string :type
|
7
|
+
t.string :transaction_id, null: false, index: { unique: true }
|
8
|
+
t.string :description
|
9
|
+
t.column :metadata, <%= json_column_type %>
|
10
|
+
t.timestamp :occurred_at, null: false, default: -> { 'NOW()' }
|
11
|
+
t.timestamps default: -> { 'NOW()' }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class <%= module_name %>::<%= class_name %> < Ledger::BaseTransaction
|
4
|
+
transaction_id do
|
5
|
+
# TODO: Generate a transaction ID
|
6
|
+
end
|
7
|
+
|
8
|
+
description do
|
9
|
+
# Optional: Provide a description of the transaction
|
10
|
+
end
|
11
|
+
|
12
|
+
occurred_at do
|
13
|
+
# Optional: Include a timestamp for the transaction (defaults to now)
|
14
|
+
end
|
15
|
+
|
16
|
+
metadata do
|
17
|
+
# Optional: Any additional metadata to be stored with the transaction (an Array or Hash)
|
18
|
+
end
|
19
|
+
|
20
|
+
double_entry do |e|
|
21
|
+
# TODO: Define entries
|
22
|
+
# e.debit = Generalis::Asset[:cash]
|
23
|
+
# e.credit = customer.accounts_receivable
|
24
|
+
# e.amount = 100.00
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
|
5
|
+
module Generalis
|
6
|
+
module Generators
|
7
|
+
class TransactionGenerator < Rails::Generators::NamedBase
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
9
|
+
|
10
|
+
def create_transaction
|
11
|
+
template 'transaction.rb.erb', "app/models/#{module_path}/#{file_name}.rb"
|
12
|
+
end
|
13
|
+
|
14
|
+
def file_name
|
15
|
+
class_name.underscore
|
16
|
+
end
|
17
|
+
|
18
|
+
def class_name
|
19
|
+
"#{name.to_s.classify.chomp('Transaction')}Transaction"
|
20
|
+
end
|
21
|
+
|
22
|
+
def module_name
|
23
|
+
module_path.classify
|
24
|
+
end
|
25
|
+
|
26
|
+
def module_path
|
27
|
+
'ledger'
|
28
|
+
end
|
29
|
+
|
30
|
+
hook_for :test_framework
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
|
5
|
+
module Rspec
|
6
|
+
module Generators
|
7
|
+
class TransactionGenerator < Rails::Generators::NamedBase
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
9
|
+
|
10
|
+
def create_transaction_spec
|
11
|
+
template 'transaction_spec.rb.erb', "spec/models/#{module_path}/#{file_name}.rb"
|
12
|
+
end
|
13
|
+
|
14
|
+
def file_name
|
15
|
+
"#{class_name.underscore}_spec"
|
16
|
+
end
|
17
|
+
|
18
|
+
def qualified_class_name
|
19
|
+
"#{module_name}::#{class_name}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def class_name
|
23
|
+
"#{name.to_s.classify.chomp('Transaction')}Transaction"
|
24
|
+
end
|
25
|
+
|
26
|
+
def module_name
|
27
|
+
module_path.classify
|
28
|
+
end
|
29
|
+
|
30
|
+
def module_path
|
31
|
+
'ledger'
|
32
|
+
end
|
33
|
+
|
34
|
+
def factory_name
|
35
|
+
class_name.underscore
|
36
|
+
end
|
37
|
+
|
38
|
+
hook_for :fixture_replacement
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helpers/resolve_account_helper'
|
4
|
+
require_relative 'helpers/resolve_amount_helper'
|
5
|
+
|
6
|
+
RSpec::Matchers.define :change_balance_of do |account, owner: nil|
|
7
|
+
include Generalis::RSpec::ResolveAccountHelper
|
8
|
+
include Generalis::RSpec::ResolveAmountHelper
|
9
|
+
|
10
|
+
match do |transaction|
|
11
|
+
transaction.prepare
|
12
|
+
|
13
|
+
account = resolve_account(account, owner: owner)
|
14
|
+
entries = transaction.entries.select { |entry| entry.account == account }
|
15
|
+
|
16
|
+
entries.any? && matches_amount?(entries)
|
17
|
+
end
|
18
|
+
|
19
|
+
chain(:by) do |amount, currency = nil|
|
20
|
+
@amount = resolve_amount(amount, currency)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param entries [Array<Generalis::Entry>]
|
24
|
+
# @return [Boolean]
|
25
|
+
def matches_amount?(entries)
|
26
|
+
return entries.group_by(&:currency).values.any? { |bucket| bucket.sum(&:net_amount).nonzero? } if @amount.nil?
|
27
|
+
|
28
|
+
entries
|
29
|
+
.select { |entry| entry.currency.casecmp(@amount.currency).zero? }
|
30
|
+
.sum(&:net_amount) == @amount
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helpers/format_helper'
|
4
|
+
require_relative 'helpers/resolve_account_helper'
|
5
|
+
require_relative 'helpers/resolve_amount_helper'
|
6
|
+
|
7
|
+
RSpec::Matchers.define :credit_account do |account, owner: nil|
|
8
|
+
include Generalis::RSpec::FormatHelper
|
9
|
+
include Generalis::RSpec::ResolveAccountHelper
|
10
|
+
include Generalis::RSpec::ResolveAmountHelper
|
11
|
+
|
12
|
+
match do |transaction|
|
13
|
+
transaction.prepare
|
14
|
+
|
15
|
+
@account = resolve_account(account, owner: owner)
|
16
|
+
entries = transaction.entries.select { |entry| entry.credit? && entry.account == @account }
|
17
|
+
|
18
|
+
entries.any? && matches_amount?(entries)
|
19
|
+
end
|
20
|
+
|
21
|
+
chain(:with_amount) do |amount, currency = nil|
|
22
|
+
@amount = resolve_amount(amount, currency)
|
23
|
+
end
|
24
|
+
|
25
|
+
failure_message do |transaction|
|
26
|
+
message = "expected transaction to credit account #{format_account(@account)}"
|
27
|
+
message += " with amount #{format_money(@amount)}" if @amount
|
28
|
+
message += "\n"
|
29
|
+
message += "\nCredits:\n"
|
30
|
+
message += transaction.entries.select(&:credit?).map { |entry| format_entry(entry) }.join("\n")
|
31
|
+
message += "\nDebits:\n"
|
32
|
+
message += transaction.entries.select(&:debit?).map { |entry| format_entry(entry) }.join("\n")
|
33
|
+
message
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param entries [Array<Generalis::Entry>]
|
37
|
+
# @return [Boolean]
|
38
|
+
def matches_amount?(entries)
|
39
|
+
return true if @amount.nil?
|
40
|
+
|
41
|
+
entries
|
42
|
+
.select { |entry| entry.currency.casecmp(@amount.currency).zero? }
|
43
|
+
.sum(&:amount) == @amount
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helpers/format_helper'
|
4
|
+
require_relative 'helpers/resolve_account_helper'
|
5
|
+
require_relative 'helpers/resolve_amount_helper'
|
6
|
+
|
7
|
+
RSpec::Matchers.define :debit_account do |account, owner: nil|
|
8
|
+
include Generalis::RSpec::FormatHelper
|
9
|
+
include Generalis::RSpec::ResolveAccountHelper
|
10
|
+
include Generalis::RSpec::ResolveAmountHelper
|
11
|
+
|
12
|
+
match do |transaction|
|
13
|
+
transaction.prepare
|
14
|
+
|
15
|
+
@account = resolve_account(account, owner: owner)
|
16
|
+
entries = transaction.entries.select { |entry| entry.debit? && entry.account == @account }
|
17
|
+
|
18
|
+
entries.any? && matches_amount?(entries)
|
19
|
+
end
|
20
|
+
|
21
|
+
chain(:with_amount) do |amount, currency = nil|
|
22
|
+
@amount = resolve_amount(amount, currency)
|
23
|
+
end
|
24
|
+
|
25
|
+
failure_message do |transaction|
|
26
|
+
message = "expected transaction to debit account #{format_account(@account)}"
|
27
|
+
message += " with amount #{format_money(@amount)}" if @amount
|
28
|
+
message += "\n"
|
29
|
+
message += "\nCredits:\n"
|
30
|
+
message += transaction.entries.select(&:credit?).map { |entry| format_entry(entry) }.join("\n")
|
31
|
+
message += "\nDebits:\n"
|
32
|
+
message += transaction.entries.select(&:debit?).map { |entry| format_entry(entry) }.join("\n")
|
33
|
+
message
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param entries [Array<Generalis::Entry>]
|
37
|
+
# @return [Boolean]
|
38
|
+
def matches_amount?(entries)
|
39
|
+
return true if @amount.nil?
|
40
|
+
|
41
|
+
entries
|
42
|
+
.select { |entry| entry.currency.casecmp(@amount.currency).zero? }
|
43
|
+
.sum(&:amount) == @amount
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helpers/format_helper'
|
4
|
+
require_relative 'helpers/resolve_account_helper'
|
5
|
+
require_relative 'helpers/resolve_amount_helper'
|
6
|
+
|
7
|
+
RSpec::Matchers.define :have_balance do |amount, currency = nil|
|
8
|
+
include Generalis::RSpec::FormatHelper
|
9
|
+
include Generalis::RSpec::ResolveAccountHelper
|
10
|
+
include Generalis::RSpec::ResolveAmountHelper
|
11
|
+
|
12
|
+
match do |account, owner: nil|
|
13
|
+
@account = resolve_account(account, owner: owner)
|
14
|
+
@amount = resolve_amount(amount, currency)
|
15
|
+
|
16
|
+
values_match?(@account.balance(@amount.currency.to_s), @amount)
|
17
|
+
end
|
18
|
+
|
19
|
+
failure_message do
|
20
|
+
message = "expected #{format_account(@account)} to have balance #{format_money(@amount)}\n"
|
21
|
+
message + "\tactual balance was #{format_money(@account.balance(@amount.currency.to_s))}"
|
22
|
+
end
|
23
|
+
|
24
|
+
failure_message_when_negated do
|
25
|
+
"expected #{format_account(@account)} not to have balance #{format_money(@amount)}"
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Generalis
|
4
|
+
module RSpec
|
5
|
+
module FormatHelper
|
6
|
+
# @param entry [Generalis::Entry]
|
7
|
+
# @return [String]
|
8
|
+
def format_entry(entry)
|
9
|
+
"\t#{format_money(entry.amount)} to #{format_account(entry.account)}"
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param account [Generalis::Account]
|
13
|
+
# @return [String]
|
14
|
+
def format_account(account)
|
15
|
+
text = "#{account.class}[:#{account.name}]"
|
16
|
+
text += " (Owner: #{account.owner.class} #{account.owner.id})" if account.owner
|
17
|
+
|
18
|
+
text
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param money [Money]
|
22
|
+
# @return [String]
|
23
|
+
def format_money(money)
|
24
|
+
"#{money.format} (#{money.currency})"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Generalis
|
4
|
+
module RSpec
|
5
|
+
module ResolveAccountHelper
|
6
|
+
# @param location [Symbol, String, Generalis::Account]
|
7
|
+
# @param owner [ActiveRecord::Base, nil]
|
8
|
+
# @return [Generalis::Account]
|
9
|
+
def resolve_account(locator, owner:)
|
10
|
+
case locator
|
11
|
+
when Generalis::Account
|
12
|
+
locator
|
13
|
+
when String, Symbol
|
14
|
+
Generalis::Account.lookup(locator, owner: owner)
|
15
|
+
else
|
16
|
+
raise ArgumentError, "Expected a Generalis::Account, String, or Symbol, got #{account.inspect}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Generalis
|
4
|
+
module RSpec
|
5
|
+
module ResolveAmountHelper
|
6
|
+
# @param amount [Money, Numeric]
|
7
|
+
# @param currency [String, nil]
|
8
|
+
# @return [Money]
|
9
|
+
def resolve_amount(amount, currency = nil)
|
10
|
+
case amount
|
11
|
+
when Money
|
12
|
+
amount
|
13
|
+
when Numeric
|
14
|
+
Money.from_amount(amount, currency)
|
15
|
+
else
|
16
|
+
raise ArgumentError, "Expected Money or Numeric, got #{amount.inspect}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|