double_entry 1.0.1 → 2.0.0.beta5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +497 -0
  3. data/README.md +107 -44
  4. data/double_entry.gemspec +22 -49
  5. data/lib/active_record/locking_extensions.rb +3 -3
  6. data/lib/active_record/locking_extensions/log_subscriber.rb +1 -1
  7. data/lib/double_entry.rb +29 -21
  8. data/lib/double_entry/account.rb +39 -46
  9. data/lib/double_entry/account_balance.rb +20 -3
  10. data/lib/double_entry/balance_calculator.rb +5 -5
  11. data/lib/double_entry/configurable.rb +1 -0
  12. data/lib/double_entry/configuration.rb +8 -2
  13. data/lib/double_entry/errors.rb +13 -13
  14. data/lib/double_entry/line.rb +7 -6
  15. data/lib/double_entry/locking.rb +5 -5
  16. data/lib/double_entry/transfer.rb +37 -30
  17. data/lib/double_entry/validation.rb +1 -0
  18. data/lib/double_entry/validation/account_fixer.rb +36 -0
  19. data/lib/double_entry/validation/line_check.rb +25 -43
  20. data/lib/double_entry/version.rb +1 -1
  21. data/lib/generators/double_entry/install/install_generator.rb +22 -1
  22. data/lib/generators/double_entry/install/templates/initializer.rb +20 -0
  23. data/lib/generators/double_entry/install/templates/migration.rb +45 -55
  24. metadata +35 -256
  25. data/.gitignore +0 -32
  26. data/.rspec +0 -2
  27. data/.travis.yml +0 -29
  28. data/.yardopts +0 -2
  29. data/Gemfile +0 -2
  30. data/Rakefile +0 -15
  31. data/lib/double_entry/reporting.rb +0 -181
  32. data/lib/double_entry/reporting/aggregate.rb +0 -110
  33. data/lib/double_entry/reporting/aggregate_array.rb +0 -76
  34. data/lib/double_entry/reporting/day_range.rb +0 -42
  35. data/lib/double_entry/reporting/hour_range.rb +0 -45
  36. data/lib/double_entry/reporting/line_aggregate.rb +0 -16
  37. data/lib/double_entry/reporting/line_aggregate_filter.rb +0 -79
  38. data/lib/double_entry/reporting/month_range.rb +0 -94
  39. data/lib/double_entry/reporting/time_range.rb +0 -59
  40. data/lib/double_entry/reporting/time_range_array.rb +0 -49
  41. data/lib/double_entry/reporting/week_range.rb +0 -107
  42. data/lib/double_entry/reporting/year_range.rb +0 -40
  43. data/script/jack_hammer +0 -210
  44. data/script/setup.sh +0 -8
  45. data/spec/active_record/locking_extensions_spec.rb +0 -110
  46. data/spec/double_entry/account_balance_spec.rb +0 -7
  47. data/spec/double_entry/account_spec.rb +0 -130
  48. data/spec/double_entry/balance_calculator_spec.rb +0 -88
  49. data/spec/double_entry/configuration_spec.rb +0 -50
  50. data/spec/double_entry/line_spec.rb +0 -80
  51. data/spec/double_entry/locking_spec.rb +0 -214
  52. data/spec/double_entry/performance/double_entry_performance_spec.rb +0 -32
  53. data/spec/double_entry/performance/reporting/aggregate_performance_spec.rb +0 -50
  54. data/spec/double_entry/reporting/aggregate_array_spec.rb +0 -123
  55. data/spec/double_entry/reporting/aggregate_spec.rb +0 -205
  56. data/spec/double_entry/reporting/line_aggregate_filter_spec.rb +0 -90
  57. data/spec/double_entry/reporting/line_aggregate_spec.rb +0 -39
  58. data/spec/double_entry/reporting/month_range_spec.rb +0 -139
  59. data/spec/double_entry/reporting/time_range_array_spec.rb +0 -169
  60. data/spec/double_entry/reporting/time_range_spec.rb +0 -45
  61. data/spec/double_entry/reporting/week_range_spec.rb +0 -103
  62. data/spec/double_entry/reporting_spec.rb +0 -181
  63. data/spec/double_entry/transfer_spec.rb +0 -93
  64. data/spec/double_entry/validation/line_check_spec.rb +0 -99
  65. data/spec/double_entry_spec.rb +0 -428
  66. data/spec/generators/double_entry/install/install_generator_spec.rb +0 -30
  67. data/spec/spec_helper.rb +0 -118
  68. data/spec/support/accounts.rb +0 -21
  69. data/spec/support/blueprints.rb +0 -43
  70. data/spec/support/database.example.yml +0 -21
  71. data/spec/support/database.travis.yml +0 -24
  72. data/spec/support/double_entry_spec_helper.rb +0 -27
  73. data/spec/support/gemfiles/Gemfile.rails-3.2.x +0 -8
  74. data/spec/support/gemfiles/Gemfile.rails-4.1.x +0 -6
  75. data/spec/support/gemfiles/Gemfile.rails-4.2.x +0 -5
  76. data/spec/support/gemfiles/Gemfile.rails-5.0.x +0 -5
  77. data/spec/support/performance_helper.rb +0 -26
  78. data/spec/support/reporting_configuration.rb +0 -6
  79. data/spec/support/schema.rb +0 -74
@@ -1,93 +1,86 @@
1
1
  # encoding: utf-8
2
+ require 'forwardable'
3
+
2
4
  module DoubleEntry
3
5
  class Account
4
6
  class << self
5
- attr_writer :accounts, :scope_identifier_max_length, :account_identifier_max_length
7
+ attr_accessor :scope_identifier_max_length, :account_identifier_max_length
8
+ attr_writer :accounts
6
9
 
7
10
  # @api private
8
11
  def accounts
9
12
  @accounts ||= Set.new
10
13
  end
11
14
 
12
- # @api private
13
- def scope_identifier_max_length
14
- @scope_identifier_max_length ||= 23
15
- end
16
-
17
- # @api private
18
- def account_identifier_max_length
19
- @account_identifier_max_length ||= 31
20
- end
21
-
22
15
  # @api private
23
16
  def account(identifier, options = {})
24
- account = accounts.find(identifier, options[:scope].present?)
25
- Instance.new(:account => account, :scope => options[:scope])
17
+ account = accounts.find(identifier, (options[:scope].present? || options[:scope_identity].present?))
18
+ Instance.new(account: account, scope: options[:scope], scope_identity: options[:scope_identity])
26
19
  end
27
20
 
28
21
  # @api private
29
22
  def currency(identifier)
30
- accounts.detect { |a| a.identifier == identifier }.try(:currency)
23
+ accounts.find_without_scope(identifier).try(:currency)
31
24
  end
32
25
  end
33
26
 
34
27
  # @api private
35
- class Set < Array
28
+ class Set
29
+ extend Forwardable
30
+
31
+ delegate [:each, :map] => :all
32
+
36
33
  def define(attributes)
37
- self << Account.new(attributes)
34
+ Account.new(attributes).tap do |account|
35
+ if find_without_scope(account.identifier)
36
+ fail DuplicateAccount
37
+ else
38
+ backing_collection[account.identifier] = account
39
+ end
40
+ end
38
41
  end
39
42
 
40
43
  def find(identifier, scoped)
41
- found_account = detect do |account|
42
- account.identifier == identifier && account.scoped? == scoped
43
- end
44
- fail UnknownAccount, "account: #{identifier} scoped?: #{scoped}" unless found_account
45
- found_account
46
- end
44
+ found_account = find_without_scope(identifier)
47
45
 
48
- def <<(account)
49
- if any? { |a| a.identifier == account.identifier }
50
- fail DuplicateAccount
46
+ if found_account && found_account.scoped? == scoped
47
+ found_account
51
48
  else
52
- super
49
+ fail UnknownAccount, "account: #{identifier} scoped?: #{scoped}"
53
50
  end
54
51
  end
55
52
 
56
- def active_record_scope_identifier(active_record_class)
57
- ActiveRecordScopeFactory.new(active_record_class).scope_identifier
53
+ def find_without_scope(identifier)
54
+ backing_collection[identifier]
58
55
  end
59
- end
60
56
 
61
- class ActiveRecordScopeFactory
62
- def initialize(active_record_class)
63
- @active_record_class = active_record_class
57
+ def all
58
+ backing_collection.values
64
59
  end
65
60
 
66
- def scope_identifier
67
- lambda do |value|
68
- case value
69
- when @active_record_class
70
- value.id
71
- when String, Fixnum
72
- value
73
- else
74
- fail AccountScopeMismatchError, "Expected instance of `#{@active_record_class}`, received instance of `#{value.class}`"
75
- end
76
- end
61
+ private
62
+
63
+ def backing_collection
64
+ @backing_collection ||= Hash.new
77
65
  end
78
66
  end
79
67
 
80
68
  class Instance
81
69
  attr_reader :account, :scope
82
- delegate :identifier, :scope_identifier, :scoped?, :positive_only, :negative_only, :currency, :to => :account
70
+ delegate :identifier, :scope_identifier, :scoped?, :positive_only, :negative_only, :currency, to: :account
83
71
 
84
72
  def initialize(args)
85
73
  @account = args[:account]
86
74
  @scope = args[:scope]
75
+ @scope_identity = args[:scope_identity]
87
76
  ensure_scope_is_valid
88
77
  end
89
78
 
90
79
  def scope_identity
80
+ @scope_identity || call_scope_identifier
81
+ end
82
+
83
+ def call_scope_identifier
91
84
  scope_identifier.call(scope).to_s if scoped?
92
85
  end
93
86
 
@@ -138,7 +131,7 @@ module DoubleEntry
138
131
 
139
132
  def ensure_scope_is_valid
140
133
  identity = scope_identity
141
- if identity && identity.length > Account.scope_identifier_max_length
134
+ if identity && Account.scope_identifier_max_length && identity.length > Account.scope_identifier_max_length
142
135
  fail ScopeIdentifierTooLongError,
143
136
  "scope identifier '#{identity}' is too long. Please limit it to #{Account.scope_identifier_max_length} characters."
144
137
  end
@@ -153,7 +146,7 @@ module DoubleEntry
153
146
  @positive_only = args[:positive_only]
154
147
  @negative_only = args[:negative_only]
155
148
  @currency = args[:currency] || Money.default_currency
156
- if identifier.length > Account.account_identifier_max_length
149
+ if Account.account_identifier_max_length && identifier.length > Account.account_identifier_max_length
157
150
  fail AccountIdentifierTooLongError,
158
151
  "account identifier '#{identifier}' is too long. Please limit it to #{Account.account_identifier_max_length} characters."
159
152
  end
@@ -8,7 +8,7 @@ module DoubleEntry
8
8
  #
9
9
  # Account balances are created on demand when transfers occur.
10
10
  class AccountBalance < ActiveRecord::Base
11
- delegate :currency, :to => :account
11
+ delegate :currency, to: :account
12
12
 
13
13
  def balance
14
14
  self[:balance] && Money.new(self[:balance], currency)
@@ -25,13 +25,30 @@ module DoubleEntry
25
25
  end
26
26
 
27
27
  def account
28
- DoubleEntry.account(self[:account].to_sym, :scope => self[:scope])
28
+ DoubleEntry.account(self[:account].to_sym, scope_identity: self[:scope])
29
29
  end
30
30
 
31
31
  def self.find_by_account(account, options = {})
32
- scope = where(:scope => account.scope_identity, :account => account.identifier.to_s)
32
+ scope = where(scope: account.scope_identity, account: account.identifier.to_s)
33
33
  scope = scope.lock(true) if options[:lock]
34
34
  scope.first
35
35
  end
36
+
37
+ # Identify the scopes with the given account identifier holding at least
38
+ # the provided minimum balance.
39
+ #
40
+ # @example Find users with at least $1,000,000 in their savings accounts
41
+ # DoubleEntry::AccountBalance.scopes_with_minimum_balance_for_account(
42
+ # 1_000_000.dollars,
43
+ # :savings,
44
+ # ) # might return the user ids: [ '1423', '12232', '34729' ]
45
+ # @param [Money] minimum_balance Minimum account balance a scope must have
46
+ # to be included in the result set.
47
+ # @param [Symbol] account_identifier
48
+ # @return [Array<String>] Scopes
49
+ #
50
+ def self.scopes_with_minimum_balance_for_account(minimum_balance, account_identifier)
51
+ where(account: account_identifier).where('balance >= ?', minimum_balance.fractional).pluck(:scope)
52
+ end
36
53
  end
37
54
  end
@@ -79,18 +79,18 @@ module DoubleEntry
79
79
  # @api private
80
80
  class RelationBuilder
81
81
  attr_reader :options
82
- delegate :account, :scope, :scope?, :from, :to, :between?, :at, :at?, :codes, :code?, :to => :options
82
+ delegate :account, :scope, :scope?, :from, :to, :between?, :at, :at?, :codes, :code?, to: :options
83
83
 
84
84
  def initialize(options)
85
85
  @options = options
86
86
  end
87
87
 
88
88
  def build
89
- lines = Line.where(:account => account)
89
+ lines = Line.where(account: account)
90
90
  lines = lines.where('created_at <= ?', at) if at?
91
- lines = lines.where(:created_at => from..to) if between?
92
- lines = lines.where(:code => codes) if code?
93
- lines = lines.where(:scope => scope) if scope?
91
+ lines = lines.where(created_at: from..to) if between?
92
+ lines = lines.where(code: codes) if code?
93
+ lines = lines.where(scope: scope) if scope?
94
94
  lines
95
95
  end
96
96
  end
@@ -41,6 +41,7 @@ module DoubleEntry
41
41
  def configuration
42
42
  @configuration ||= self::Configuration.new
43
43
  end
44
+ alias config configuration
44
45
 
45
46
  def configure
46
47
  yield(configuration)
@@ -3,6 +3,12 @@ module DoubleEntry
3
3
  include Configurable
4
4
 
5
5
  class Configuration
6
+ attr_accessor :json_metadata
7
+
8
+ def initialize
9
+ @json_metadata = false
10
+ end
11
+
6
12
  delegate(
7
13
  :accounts,
8
14
  :accounts=,
@@ -10,7 +16,7 @@ module DoubleEntry
10
16
  :scope_identifier_max_length=,
11
17
  :account_identifier_max_length,
12
18
  :account_identifier_max_length=,
13
- :to => 'DoubleEntry::Account',
19
+ to: 'DoubleEntry::Account',
14
20
  )
15
21
 
16
22
  delegate(
@@ -18,7 +24,7 @@ module DoubleEntry
18
24
  :transfers=,
19
25
  :code_max_length,
20
26
  :code_max_length=,
21
- :to => 'DoubleEntry::Transfer',
27
+ to: 'DoubleEntry::Transfer',
22
28
  )
23
29
 
24
30
  def define_accounts
@@ -1,16 +1,16 @@
1
1
  # encoding: utf-8
2
2
  module DoubleEntry
3
- class UnknownAccount < RuntimeError; end
4
- class AccountIdentifierTooLongError < RuntimeError; end
5
- class ScopeIdentifierTooLongError < RuntimeError; end
6
- class TransferNotAllowed < RuntimeError; end
7
- class TransferIsNegative < RuntimeError; end
8
- class TransferCodeTooLongError < RuntimeError; end
9
- class DuplicateAccount < RuntimeError; end
10
- class DuplicateTransfer < RuntimeError; end
11
- class AccountWouldBeSentNegative < RuntimeError; end
12
- class AccountWouldBeSentPositiveError < RuntimeError; end
13
- class MismatchedCurrencies < RuntimeError; end
14
- class MissingAccountError < RuntimeError; end
15
- class AccountScopeMismatchError < RuntimeError; end
3
+ class DoubleEntryError < RuntimeError; end
4
+ class UnknownAccount < DoubleEntryError; end
5
+ class AccountIdentifierTooLongError < DoubleEntryError; end
6
+ class ScopeIdentifierTooLongError < DoubleEntryError; end
7
+ class TransferNotAllowed < DoubleEntryError; end
8
+ class TransferIsNegative < DoubleEntryError; end
9
+ class TransferCodeTooLongError < DoubleEntryError; end
10
+ class DuplicateAccount < DoubleEntryError; end
11
+ class DuplicateTransfer < DoubleEntryError; end
12
+ class AccountWouldBeSentNegative < DoubleEntryError; end
13
+ class AccountWouldBeSentPositiveError < DoubleEntryError; end
14
+ class MismatchedCurrencies < DoubleEntryError; end
15
+ class MissingAccountError < DoubleEntryError; end
16
16
  end
@@ -55,8 +55,9 @@ module DoubleEntry
55
55
  # by account, or account and code, over a particular period.
56
56
  #
57
57
  class Line < ActiveRecord::Base
58
- belongs_to :detail, :polymorphic => true
59
- has_many :metadata, :class_name => 'DoubleEntry::LineMetadata'
58
+ belongs_to :detail, polymorphic: true, required: false
59
+ has_many :metadata, class_name: 'DoubleEntry::LineMetadata' unless -> { DoubleEntry.config.json_metadata }
60
+ scope :with_id_greater_than, ->(id) { where('id > ?', id) }
60
61
 
61
62
  def amount
62
63
  self[:amount] && Money.new(self[:amount], currency)
@@ -74,12 +75,12 @@ module DoubleEntry
74
75
  self[:balance] = (money && money.fractional)
75
76
  end
76
77
 
77
- def save(*)
78
+ def save(**)
78
79
  check_balance_will_remain_valid
79
80
  super
80
81
  end
81
82
 
82
- def save!(*)
83
+ def save!(**)
83
84
  check_balance_will_remain_valid
84
85
  super
85
86
  end
@@ -101,7 +102,7 @@ module DoubleEntry
101
102
  end
102
103
 
103
104
  def account
104
- DoubleEntry.account(self[:account].to_sym, :scope => scope)
105
+ DoubleEntry.account(self[:account].to_sym, scope_identity: scope)
105
106
  end
106
107
 
107
108
  def currency
@@ -116,7 +117,7 @@ module DoubleEntry
116
117
  end
117
118
 
118
119
  def partner_account
119
- DoubleEntry.account(self[:partner_account].to_sym, :scope => partner_scope)
120
+ DoubleEntry.account(self[:partner_account].to_sym, scope_identity: partner_scope)
120
121
  end
121
122
 
122
123
  def partner
@@ -32,14 +32,14 @@ module DoubleEntry
32
32
  #
33
33
  # The transaction must be the outermost transaction to ensure data integrity. A
34
34
  # LockMustBeOutermostTransaction will be raised if it isn't.
35
- def self.lock_accounts(*accounts)
35
+ def self.lock_accounts(*accounts, &block)
36
36
  lock = Lock.new(accounts)
37
37
 
38
38
  if lock.in_a_locked_transaction?
39
39
  lock.ensure_locked!
40
- yield
40
+ block.call
41
41
  else
42
- lock.perform_lock(&Proc.new)
42
+ lock.perform_lock(&block)
43
43
  end
44
44
 
45
45
  rescue ActiveRecord::StatementInvalid => exception
@@ -149,7 +149,7 @@ module DoubleEntry
149
149
  # If one or more account balance records don't exist, set
150
150
  # accounts_with_balances to the corresponding accounts, and return false.
151
151
  def grab_locks
152
- account_balances = @accounts.map { |account| AccountBalance.find_by_account(account, :lock => true) }
152
+ account_balances = @accounts.map { |account| AccountBalance.find_by_account(account, lock: true) }
153
153
 
154
154
  if account_balances.any?(&:nil?)
155
155
  @accounts_without_balances = @accounts.zip(account_balances).
@@ -168,7 +168,7 @@ module DoubleEntry
168
168
  # Get the initial balance from the lines table.
169
169
  balance = account.balance
170
170
  # Try to create the balance record, but ignore it if someone else has done it in the meantime.
171
- AccountBalance.create_ignoring_duplicates!(:account => account, :balance => balance)
171
+ AccountBalance.create_ignoring_duplicates!(account: account, balance: balance)
172
172
  end
173
173
  end
174
174
  end
@@ -1,19 +1,17 @@
1
1
  # encoding: utf-8
2
+ require 'forwardable'
3
+
2
4
  module DoubleEntry
3
5
  class Transfer
4
6
  class << self
5
- attr_writer :transfers, :code_max_length
7
+ attr_accessor :code_max_length
8
+ attr_writer :transfers
6
9
 
7
10
  # @api private
8
11
  def transfers
9
12
  @transfers ||= Set.new
10
13
  end
11
14
 
12
- # @api private
13
- def code_max_length
14
- @code_max_length ||= 47
15
- end
16
-
17
15
  # @api private
18
16
  def transfer(amount, options = {})
19
17
  fail TransferIsNegative if amount.negative?
@@ -25,37 +23,43 @@ module DoubleEntry
25
23
  end
26
24
 
27
25
  # @api private
28
- class Set < Array
26
+ class Set
27
+ extend Forwardable
28
+ delegate [:each, :map] => :all
29
+
29
30
  def define(attributes)
30
- self << Transfer.new(attributes)
31
+ Transfer.new(attributes).tap do |transfer|
32
+ key = [transfer.from, transfer.to, transfer.code]
33
+ if _find(*key)
34
+ fail DuplicateTransfer
35
+ else
36
+ backing_collection[key] = transfer
37
+ end
38
+ end
31
39
  end
32
40
 
33
- def find(from, to, code)
34
- _find(from.identifier, to.identifier, code)
41
+ def find(from_account, to_account, code)
42
+ _find(from_account.identifier, to_account.identifier, code)
35
43
  end
36
44
 
37
- def find!(from, to, code)
38
- find(from, to, code).tap do |transfer|
39
- fail TransferNotAllowed, [from.identifier, to.identifier, code].inspect unless transfer
45
+ def find!(from_account, to_account, code)
46
+ find(from_account, to_account, code).tap do |transfer|
47
+ fail TransferNotAllowed, [from_account.identifier, to_account.identifier, code].inspect unless transfer
40
48
  end
41
49
  end
42
50
 
43
- def <<(transfer)
44
- if _find(transfer.from, transfer.to, transfer.code)
45
- fail DuplicateTransfer
46
- else
47
- super(transfer)
48
- end
51
+ def all
52
+ backing_collection.values
49
53
  end
50
54
 
51
55
  private
52
56
 
57
+ def backing_collection
58
+ @backing_collection ||= Hash.new
59
+ end
60
+
53
61
  def _find(from, to, code)
54
- detect do |transfer|
55
- transfer.from == from &&
56
- transfer.to == to &&
57
- transfer.code == code
58
- end
62
+ backing_collection[[from, to, code]]
59
63
  end
60
64
  end
61
65
 
@@ -65,7 +69,7 @@ module DoubleEntry
65
69
  @code = attributes[:code]
66
70
  @from = attributes[:from]
67
71
  @to = attributes[:to]
68
- if code.length > Transfer.code_max_length
72
+ if Transfer.code_max_length && code.length > Transfer.code_max_length
69
73
  fail TransferCodeTooLongError,
70
74
  "transfer code '#{code}' is too long. Please limit it to #{Transfer.code_max_length} characters."
71
75
  end
@@ -84,12 +88,12 @@ module DoubleEntry
84
88
  fail MismatchedCurrencies, "Mismatched currency (#{to_account.currency} <> #{from_account.currency})"
85
89
  end
86
90
  Locking.lock_accounts(from_account, to_account) do
87
- credit, debit = create_lines(amount, code, detail, from_account, to_account)
88
- create_line_metadata(credit, debit, metadata) if metadata
91
+ credit, debit = create_lines(amount, code, detail, from_account, to_account, metadata)
92
+ create_line_metadata(credit, debit, metadata) if metadata && !DoubleEntry.config.json_metadata
89
93
  end
90
94
  end
91
95
 
92
- def create_lines(amount, code, detail, from_account, to_account)
96
+ def create_lines(amount, code, detail, from_account, to_account, metadata)
93
97
  credit, debit = Line.new, Line.new
94
98
 
95
99
  credit_balance = Locking.balance_for_locked_account(from_account)
@@ -103,6 +107,7 @@ module DoubleEntry
103
107
  credit.code, debit.code = code, code
104
108
  credit.detail, debit.detail = detail, detail
105
109
  credit.balance, debit.balance = credit_balance.balance, debit_balance.balance
110
+ credit.metadata, debit.metadata = metadata, metadata if DoubleEntry.config.json_metadata
106
111
 
107
112
  credit.partner_account, debit.partner_account = to_account, from_account
108
113
 
@@ -115,8 +120,10 @@ module DoubleEntry
115
120
 
116
121
  def create_line_metadata(credit, debit, metadata)
117
122
  metadata.each_pair do |key, value|
118
- LineMetadata.create!(:line => credit, :key => key, :value => value)
119
- LineMetadata.create!(:line => debit, :key => key, :value => value)
123
+ Array(value).each do |each_value|
124
+ LineMetadata.create!(line: credit, key: key, value: each_value)
125
+ LineMetadata.create!(line: debit, key: key, value: each_value)
126
+ end
120
127
  end
121
128
  end
122
129
  end