aloe 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.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/Rakefile +26 -0
  4. data/lib/aloe.rb +14 -0
  5. data/lib/aloe/account.rb +176 -0
  6. data/lib/aloe/account_configuration.rb +21 -0
  7. data/lib/aloe/account_repository.rb +45 -0
  8. data/lib/aloe/engine.rb +6 -0
  9. data/lib/aloe/entry.rb +59 -0
  10. data/lib/aloe/inoperable_account_error.rb +11 -0
  11. data/lib/aloe/insufficient_balance_error.rb +14 -0
  12. data/lib/aloe/invalid_amount_error.rb +3 -0
  13. data/lib/aloe/invalid_currency_error.rb +16 -0
  14. data/lib/aloe/ledger.rb +83 -0
  15. data/lib/aloe/ledger_entry.rb +83 -0
  16. data/lib/aloe/reports/account_history.rb +32 -0
  17. data/lib/aloe/reports/accounts_list.rb +39 -0
  18. data/lib/aloe/transaction.rb +80 -0
  19. data/lib/aloe/transaction_rollback.rb +34 -0
  20. data/lib/aloe/version.rb +3 -0
  21. data/lib/generators/aloe/aloe_generator.rb +17 -0
  22. data/lib/generators/aloe/templates/migration.rb +44 -0
  23. data/lib/tasks/aloe_tasks.rake +24 -0
  24. data/spec/aloe/account_configuration_spec.rb +21 -0
  25. data/spec/aloe/account_integration_spec.rb +144 -0
  26. data/spec/aloe/account_spec.rb +189 -0
  27. data/spec/aloe/accounting_integration_spec.rb +172 -0
  28. data/spec/aloe/entry_spec.rb +43 -0
  29. data/spec/aloe/ledger_entry_spec.rb +105 -0
  30. data/spec/aloe/ledger_spec.rb +98 -0
  31. data/spec/aloe/transaction_rollback_spec.rb +49 -0
  32. data/spec/aloe/transaction_spec.rb +59 -0
  33. data/spec/dummy/Rakefile +6 -0
  34. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  35. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  36. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  37. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  38. data/spec/dummy/app/models/user.rb +2 -0
  39. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  40. data/spec/dummy/bin/bundle +3 -0
  41. data/spec/dummy/bin/rails +4 -0
  42. data/spec/dummy/bin/rake +4 -0
  43. data/spec/dummy/config.ru +4 -0
  44. data/spec/dummy/config/application.rb +23 -0
  45. data/spec/dummy/config/boot.rb +5 -0
  46. data/spec/dummy/config/database.yml +25 -0
  47. data/spec/dummy/config/environment.rb +5 -0
  48. data/spec/dummy/config/environments/development.rb +29 -0
  49. data/spec/dummy/config/environments/production.rb +80 -0
  50. data/spec/dummy/config/environments/test.rb +36 -0
  51. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  52. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  53. data/spec/dummy/config/initializers/inflections.rb +16 -0
  54. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  55. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  56. data/spec/dummy/config/initializers/session_store.rb +3 -0
  57. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  58. data/spec/dummy/config/locales/en.yml +23 -0
  59. data/spec/dummy/config/routes.rb +56 -0
  60. data/spec/dummy/db/development.sqlite3 +0 -0
  61. data/spec/dummy/db/migrate/20131003200954_create_users.rb +8 -0
  62. data/spec/dummy/db/migrate/20131003203647_create_aloe_tables.rb +43 -0
  63. data/spec/dummy/db/schema.rb +63 -0
  64. data/spec/dummy/log/test.log +3308 -0
  65. data/spec/dummy/public/404.html +58 -0
  66. data/spec/dummy/public/422.html +58 -0
  67. data/spec/dummy/public/500.html +57 -0
  68. data/spec/dummy/public/favicon.ico +0 -0
  69. data/spec/dummy/test/fixtures/users.yml +11 -0
  70. data/spec/dummy/test/models/user_test.rb +7 -0
  71. data/spec/spec_helper.rb +19 -0
  72. data/spec/support/account_helpers.rb +17 -0
  73. data/spec/support/user.rb +1 -0
  74. metadata +278 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f4a87bfd19ba538180dce803adff1c2f305f16da
4
+ data.tar.gz: bb678013f56e69ba1c22f96ed892ff355978545e
5
+ SHA512:
6
+ metadata.gz: a150a76f4770ca11ebb642ad73a6f00b063535cf450ae903ffc9fc8fd4fe23c5672459407fe89925860b494bac524bbb6e0e10d7fc609773e55a601f525ae916
7
+ data.tar.gz: 63166ea20295695082ba9d6f2e43ea905226bfcc23f3fb60ecf32870ee0730f60d2632d8f9491c72a89dbdadd89b3571f14b2b4f9ed09473c3b35de87655d9f0
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,26 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Aloe'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ Bundler::GemHelper.install_tasks
21
+
22
+ require 'rspec/core/rake_task'
23
+ RSpec::Core::RakeTask.new(:spec)
24
+
25
+ task :default => :spec
26
+
@@ -0,0 +1,14 @@
1
+ require 'aloe/ledger'
2
+ require 'aloe/engine'
3
+ require 'aloe/reports/account_history'
4
+ require 'aloe/reports/accounts_list'
5
+
6
+ module Aloe
7
+
8
+ ROLLBACK_TRANSACTION = 'rollback'.freeze
9
+
10
+ def self.table_name_prefix
11
+ 'aloe_'
12
+ end
13
+
14
+ end
@@ -0,0 +1,176 @@
1
+ require 'aloe/account_repository'
2
+ require 'aloe/account_configuration'
3
+ require 'aloe/entry'
4
+ require 'active_record'
5
+ require 'money'
6
+ require 'state_machine'
7
+ require 'active_support/core_ext/object/with_options'
8
+
9
+ module Aloe
10
+ class Account < ActiveRecord::Base
11
+
12
+ extend Aloe::AccountRepository
13
+
14
+ # Associations
15
+
16
+ belongs_to :owner, polymorphic: true
17
+ has_many :entries, class_name: "Aloe::Entry", include: :account
18
+ has_many :debit_transactions, through: :entries
19
+ has_many :credit_transactions, through: :entries
20
+
21
+ # Validations
22
+
23
+ validates :currency, presence: true
24
+ with_options scope: :currency do |opts|
25
+ opts.validates_uniqueness_of :name, if: ->{ name? }
26
+ end
27
+ validate :uniqueness_of_owner
28
+
29
+ # State machine
30
+
31
+ # An account can have following states:
32
+ # * open - can be manipulated
33
+ # * suspended - cannot be manipulated, needs to be unsuspended first
34
+ # * closed - cannot be manipulated, only with 0 balance
35
+ state_machine :state, initial: :open do
36
+ event :close do
37
+ transition all => :closed, if: :closeable?
38
+ end
39
+
40
+ event :reopen do
41
+ transition :closed => :open
42
+ end
43
+
44
+ event :suspend do
45
+ transition all => :suspended
46
+ end
47
+
48
+ event :unsuspend do
49
+ transition :suspended => :open
50
+ end
51
+ end
52
+
53
+ # Instance methods
54
+
55
+ serialize :configuration, Aloe::AccountConfiguration
56
+
57
+ delegate :allow_negative_balance, to: :configuration
58
+
59
+ # Return the balance of account
60
+ #
61
+ # @return [Money] The balance
62
+ def balance
63
+ Money.new read_attribute(:balance), currency
64
+ end
65
+
66
+ # Computes the balance of the account at the requested date and time.
67
+ # Returns nil if the account did not exist at that time.
68
+ #
69
+ # @param [Time] the date and time
70
+ # @return [Money] the past balance
71
+ def balance_at(time)
72
+ return nil unless created_at <= time
73
+ amount = entries.where('created_at >= ?', time).sum(&:amount)
74
+ offset = amount == 0 ? Money.new(0, currency) : amount
75
+ balance - offset
76
+ end
77
+
78
+ # Does account have minimum given balance?
79
+ #
80
+ # @param [Money, Fixnum] amount Amount in question in cents
81
+ # @param [NilClass, Symbol] option Option
82
+ # @return [true, false]
83
+ def balance_of?(amount, option = nil)
84
+ reload if option == :reload
85
+ cents_amount = amount.respond_to?(:cents) ? amount.cents : amount
86
+ read_attribute(:balance) >= cents_amount
87
+ end
88
+
89
+ # Can the account be closed?
90
+ #
91
+ # An account can be closed only if the balance is 0.
92
+ #
93
+ # @return [true, false]
94
+ def closeable?
95
+ balance.zero?
96
+ end
97
+
98
+ # Creates entry in the account.
99
+ #
100
+ # Creates new entry and modified the balance.
101
+ #
102
+ # @param [Fixnum] cents_amount Amount in cents
103
+ # @return [Aloe::Entry] Created entry
104
+ def create_entry(cents_amount)
105
+ with_lock(true) do
106
+ if cents_amount < 0 && !debit_possible?(-cents_amount)
107
+ raise Aloe::InsufficientBalanceError.new(self, -cents_amount)
108
+ end
109
+ entry = entries.create! amount: cents_amount
110
+ increment! :balance, cents_amount
111
+ entry
112
+ end
113
+ end
114
+
115
+ # Is account in given currency?
116
+ #
117
+ # @param [Currency, Symbol, String] currency_in_question
118
+ # @return [true, false]
119
+ def currency?(currency_in_question)
120
+ currency.to_s == currency_in_question.to_s
121
+ end
122
+
123
+ # Is the debit of given amount possible?
124
+ #
125
+ # @param [Money, Fixnum] amount Amount in question in cents
126
+ # @return [true, false]
127
+ def debit_possible?(amount)
128
+ allow_negative_balance ? true : balance_of?(amount, :reload)
129
+ end
130
+
131
+ # Rolls back all transactions on this account.
132
+ def rollback_all
133
+ transactions = entries.map &:transaction
134
+ transactions.map &:rollback
135
+ end
136
+
137
+ # Return string representation of account.
138
+ #
139
+ # @return [String]
140
+ def to_s
141
+ name? ? name : owner_type_and_id
142
+ end
143
+
144
+ # Return account turnover over given period of time.
145
+ #
146
+ # @param [Range] period
147
+ # @return [Money]
148
+ def turnover(period)
149
+ turnover = entries.where(created_at: period).sum &:amount
150
+ turnover == 0 ? Money.new(0, currency) : turnover
151
+ end
152
+
153
+ # Return account owner type and it's ID.
154
+ #
155
+ # @return [String]
156
+ def owner_type_and_id
157
+ "#{owner_type} #{owner_id}" if owner.present?
158
+ end
159
+
160
+ protected
161
+
162
+ def uniqueness_of_owner
163
+ if has_conflicting_account_for_owner?
164
+ message = I18n.t('aloe.accounts.errors.owner_already_has_account')
165
+ errors.add(:owner, message)
166
+ end
167
+ end
168
+
169
+ def has_conflicting_account_for_owner?
170
+ new_record? && owner.present? &&
171
+ self.class.owner(owner).currency(currency).where(name: name).exists?
172
+ end
173
+
174
+ end
175
+ end
176
+
@@ -0,0 +1,21 @@
1
+ module Aloe
2
+ class AccountConfiguration
3
+
4
+ attr_writer :allow_negative_balance
5
+
6
+ def initialize(attrs = {})
7
+ attrs.each do |k ,v|
8
+ public_send :"#{k}=", v
9
+ end
10
+ end
11
+
12
+ # Does the account allow negative balance?
13
+ # Defaults to true if no values is provided
14
+ #
15
+ # @return [true, false]
16
+ def allow_negative_balance
17
+ @allow_negative_balance == false ? false : true
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,45 @@
1
+ module Aloe
2
+ module AccountRepository
3
+
4
+ # Scope by currency.
5
+ #
6
+ # @param [String, Symbol] currency Currency symbol
7
+ # @return [ActiveRecord::Relation]
8
+ def currency(currency)
9
+ where(currency: currency)
10
+ end
11
+
12
+ # Default scope excludes closed accounts.
13
+ def default_scope
14
+ where('state != ?', 'closed')
15
+ end
16
+
17
+ # Scope to closed accounts.
18
+ #
19
+ # @return [ActiveRecord::Relation]
20
+ def closed
21
+ unscoped.with_state('closed')
22
+ end
23
+
24
+ # Scope by owner.
25
+ #
26
+ # @param [ActiveRecord::Base] owner
27
+ # @return [ActiveRecord::Relation]
28
+ def owner(owner)
29
+ where(owner_type: owner.class.model_name.to_s, owner_id: owner.id)
30
+ end
31
+
32
+ # Return the trial balance.
33
+ #
34
+ # Trial balance is balance of all accounts in the system
35
+ # combined. It should at all times be 0. If it's not, there
36
+ # is an error in accounts somewhere.
37
+ #
38
+ # @param [String, Symbol] currency Currency symbol
39
+ # @return [Fixnum] Zero if everything's fine
40
+ def trial_balance(currency)
41
+ currency(currency).sum :balance
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,6 @@
1
+ module Aloe
2
+
3
+ class Engine < Rails::Engine
4
+ end
5
+
6
+ end
@@ -0,0 +1,59 @@
1
+ require 'aloe/account'
2
+ require 'aloe/transaction'
3
+ require 'active_record'
4
+ require 'money'
5
+
6
+ module Aloe
7
+ class Entry < ActiveRecord::Base
8
+
9
+ # Scopes
10
+
11
+ default_scope do
12
+ order('aloe_entries.created_at DESC')
13
+ end
14
+
15
+ scope :withdrawals, -> { where('amount < 0') }
16
+ scope :deposits, -> { where('amount > 0') }
17
+
18
+ # Associations
19
+
20
+ belongs_to :account, class_name: "Aloe::Account"
21
+ has_one :credit_transaction, class_name: "Aloe::Transaction",
22
+ foreign_key: "credit_entry_id"
23
+ has_one :debit_transaction, class_name: "Aloe::Transaction",
24
+ foreign_key: "debit_entry_id"
25
+
26
+ # Instance methods
27
+
28
+ delegate :currency, to: :account
29
+
30
+ # Return the amount of entry
31
+ #
32
+ # @return [Money] The amount
33
+ def amount
34
+ Money.new read_attribute(:amount), currency
35
+ end
36
+ #
37
+ # Return the related transaction.
38
+ #
39
+ # @return [Aloe::Transaction]
40
+ def transaction
41
+ credit_transaction || debit_transaction
42
+ end
43
+
44
+ # Is the entry deposit of funds?
45
+ #
46
+ # @return [true, false]
47
+ def deposit?
48
+ !withdrawal?
49
+ end
50
+
51
+ # Is the entry withdrawal of funds?
52
+ #
53
+ # @return [true, false]
54
+ def withdrawal?
55
+ amount.negative?
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,11 @@
1
+ module Aloe
2
+ class InoperableAccountError < StandardError
3
+ def initialize(account)
4
+ @account = account
5
+ end
6
+
7
+ def to_s
8
+ "Account #{@account} is inoperable! (account state: #{@account.state})"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ module Aloe
2
+ class InsufficientBalanceError < StandardError
3
+
4
+ def initialize(account, expected_balance)
5
+ @account, @expected_balance = account, expected_balance
6
+ end
7
+
8
+ def to_s
9
+ "Account #{@account} has not enough balance" +
10
+ " - required at least #{@expected_balance}!"
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module Aloe
2
+ class InvalidAmountError < StandardError; end
3
+ end
@@ -0,0 +1,16 @@
1
+ module Aloe
2
+ class InvalidCurrencyError < StandardError
3
+
4
+ def initialize(credit_account, debit_account, currency)
5
+ @credit_account = credit_account
6
+ @debit_account = debit_account
7
+ @currency = currency
8
+ end
9
+
10
+ def to_s
11
+ "Different currencies on accounts #{@credit_account}, #{@debit_account}" +
12
+ " - expected #{@currency}."
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,83 @@
1
+ require 'aloe/account'
2
+ require 'aloe/transaction'
3
+ require 'aloe/ledger_entry'
4
+ require 'money'
5
+
6
+ module Aloe
7
+ # Ledger is the façade interface to the account. When manipulating accounts
8
+ # (eg. creating transactions between accounts) you should use the Ledger and
9
+ # not the underlying models.
10
+ module Ledger
11
+
12
+ extend self
13
+
14
+ attr_writer :account_scope
15
+
16
+ # Returns an account for given owner.
17
+ #
18
+ # @param [Object, Symbol] owner Account owner or account name
19
+ # @param [String, Symbol] currency Currency symbol
20
+ # @return [Aloe::Account, nil] Account which belongs to given object
21
+ # or nil
22
+ def find_account(owner, currency = default_currency)
23
+ scope_for_owner(owner).currency(currency.to_s).first
24
+ end
25
+
26
+ # Returns accounts for a given owner.
27
+ #
28
+ # @param [Object, Symbol] owner Account owner or account name
29
+ # @param [String, Symbol] currency Currency symbol
30
+ # @return [Aloe::Account, nil] Array of accounts which belongs to
31
+ # given object
32
+ def find_accounts(owner, currency = nil)
33
+ scope = scope_for_owner(owner)
34
+ currency ? scope.currency(currency) : scope
35
+ end
36
+
37
+ # Creates entry in the ledger.
38
+ #
39
+ # Creates entries in both credit and debit account and linking
40
+ # transaction between those two entries. Credit and debit accounts
41
+ # and given amount have to be in the same currency otherwise an
42
+ # exception is raised.
43
+ #
44
+ # @param [Money] amount The amount of money
45
+ # @param [Hash] options Options
46
+ # @option options [Aloe::Account] :from Account which to debit
47
+ # @option options [Aloe::Account] :to Account which to credit
48
+ # @option options [Fixnum] :type Type of transaction
49
+ # @return [Aloe::Transaction]
50
+ def create_entry(amount, options)
51
+ Aloe::LedgerEntry.new(amount, options).create!
52
+ end
53
+
54
+ # Return the default currency that gets used when no currency is specified.
55
+ #
56
+ # @return [String]
57
+ def default_currency
58
+ Money.default_currency.to_s
59
+ end
60
+
61
+ protected
62
+
63
+ # Return the scope for account with given owner.
64
+ #
65
+ # @param [Object]
66
+ # @return [ActiveRecord::Relation]
67
+ def scope_for_owner(owner)
68
+ if owner.class.respond_to?(:model_name)
69
+ account_scope.owner(owner)
70
+ else
71
+ account_scope.where(name: owner.to_s.titleize)
72
+ end
73
+ end
74
+
75
+ # Return the scope for Accounts repository.
76
+ #
77
+ # @return [ActiveRecord::Relation]
78
+ def account_scope
79
+ @account_scope ||= Aloe::Account.unscoped
80
+ end
81
+
82
+ end
83
+ end