generalis 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|