double_entry 1.0.1 → 2.0.0.beta5

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 (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