dscf-banking 0.5.2 → 0.5.3
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0086738f163c88e967b2850c3b0c2e29804725e059996dd2edfd152a98c874b5'
|
|
4
|
+
data.tar.gz: 2bc163e912db1cb78a491610f88efc8d99f8455f034ed90f64031628bce1375c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 970cd3f5b838bcd3a5c74a9b2511569cd4ec7eeb7ecf28d90e32d9efda437707214514088d562a2e35bff1a43f7dc8b89a5abff66c56071a5ff3f2e2f3abc832
|
|
7
|
+
data.tar.gz: 29f3c5f3299b0b1dc1da0cfb11594f22a0c2970865482701097bd0492552165a90a76817b8acef394f63e4de15a5758571635236edd482f633ed700117af684f
|
|
@@ -426,7 +426,41 @@ module Dscf::Banking
|
|
|
426
426
|
errors << "transactions must be an array"
|
|
427
427
|
end
|
|
428
428
|
|
|
429
|
+
if params[:initial_balance].present? && !valid_decimal?(params[:initial_balance])
|
|
430
|
+
errors << "initial_balance must be a valid number"
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
start_date = nil
|
|
434
|
+
end_date = nil
|
|
435
|
+
|
|
436
|
+
if params[:start_date].present?
|
|
437
|
+
start_date = parse_iso_date(params[:start_date])
|
|
438
|
+
errors << "start_date must be a valid date" unless start_date
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
if params[:end_date].present?
|
|
442
|
+
end_date = parse_iso_date(params[:end_date])
|
|
443
|
+
errors << "end_date must be a valid date" unless end_date
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
if start_date && end_date && start_date > end_date
|
|
447
|
+
errors << "start_date must be before or equal to end_date"
|
|
448
|
+
end
|
|
449
|
+
|
|
429
450
|
errors
|
|
430
451
|
end
|
|
452
|
+
|
|
453
|
+
def valid_decimal?(value)
|
|
454
|
+
BigDecimal(value.to_s)
|
|
455
|
+
true
|
|
456
|
+
rescue ArgumentError
|
|
457
|
+
false
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def parse_iso_date(value)
|
|
461
|
+
Date.iso8601(value.to_s)
|
|
462
|
+
rescue Date::Error
|
|
463
|
+
nil
|
|
464
|
+
end
|
|
431
465
|
end
|
|
432
466
|
end
|
|
@@ -12,15 +12,16 @@ module Dscf::Banking
|
|
|
12
12
|
@initial_balance = decimal(initial_balance)
|
|
13
13
|
@start_date = to_date(start_date)
|
|
14
14
|
@end_date = to_date(end_date)
|
|
15
|
+
@transactions_by_date = {}
|
|
15
16
|
@transactions = normalize_transactions(transactions || [])
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def call
|
|
19
20
|
raise ArgumentError, "start_date must be before or equal to end_date" if @start_date > @end_date
|
|
20
21
|
|
|
21
|
-
daily_rate = annual_interest_rate / BigDecimal(day_basis.to_s)
|
|
22
22
|
balance = @initial_balance
|
|
23
23
|
gross_interest = BigDecimal("0")
|
|
24
|
+
accrued_interest = BigDecimal("0")
|
|
24
25
|
daily_breakdown = []
|
|
25
26
|
|
|
26
27
|
(@start_date..@end_date).each do |date|
|
|
@@ -28,14 +29,25 @@ module Dscf::Banking
|
|
|
28
29
|
daily_transaction_sum = daily_transactions.sum(BigDecimal("0")) { |transaction| transaction[:signed_amount] }
|
|
29
30
|
balance += daily_transaction_sum
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
daily_rate = annual_interest_rate_for(balance) / BigDecimal(day_basis.to_s)
|
|
33
|
+
raw_daily_interest = eligible_for_interest?(date, balance) ? balance * daily_rate : BigDecimal("0")
|
|
34
|
+
accrued_interest += raw_daily_interest
|
|
35
|
+
|
|
36
|
+
posted_daily_interest = BigDecimal("0")
|
|
37
|
+
if accrual_posting_due?(date)
|
|
38
|
+
posted_daily_interest = accrued_interest
|
|
39
|
+
accrued_interest = BigDecimal("0")
|
|
40
|
+
gross_interest += posted_daily_interest
|
|
41
|
+
|
|
42
|
+
# Compound mode credits posted interest into principal for subsequent days.
|
|
43
|
+
balance += posted_daily_interest if compound_calculation? && compounding_due?(date)
|
|
44
|
+
end
|
|
33
45
|
|
|
34
46
|
daily_breakdown << {
|
|
35
47
|
date: date.iso8601,
|
|
36
48
|
balance: money(balance),
|
|
37
49
|
transaction_total: money(daily_transaction_sum),
|
|
38
|
-
daily_interest: money(
|
|
50
|
+
daily_interest: money(posted_daily_interest)
|
|
39
51
|
}
|
|
40
52
|
end
|
|
41
53
|
|
|
@@ -64,6 +76,10 @@ module Dscf::Banking
|
|
|
64
76
|
configured.present? ? decimal(configured) : DEFAULT_ANNUAL_INTEREST_RATE
|
|
65
77
|
end
|
|
66
78
|
|
|
79
|
+
def annual_interest_rate_for(balance)
|
|
80
|
+
interest_rate_tier_for(balance)&.yield_self { |tier| decimal(tier.interest_rate) } || annual_interest_rate
|
|
81
|
+
end
|
|
82
|
+
|
|
67
83
|
def tax_rate
|
|
68
84
|
configured = interest_configuration&.income_tax_rate
|
|
69
85
|
configured.present? ? decimal(configured) : DEFAULT_TAX_RATE
|
|
@@ -78,10 +94,76 @@ module Dscf::Banking
|
|
|
78
94
|
end
|
|
79
95
|
end
|
|
80
96
|
|
|
97
|
+
def compound_calculation?
|
|
98
|
+
interest_configuration&.calculation_method == "compound"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def accrual_posting_due?(date)
|
|
102
|
+
return date == date.end_of_month || date == @end_date if monthly_accrual_mode?
|
|
103
|
+
|
|
104
|
+
true
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def compounding_due?(date)
|
|
108
|
+
return date == date.end_of_month || date == @end_date if monthly_compounding_mode?
|
|
109
|
+
|
|
110
|
+
true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def monthly_accrual_mode?
|
|
114
|
+
interest_configuration&.accrual_frequency == "monthly"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def monthly_compounding_mode?
|
|
118
|
+
interest_configuration&.compounding_period == "monthly"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def eligible_for_interest?(date, balance)
|
|
122
|
+
meets_minimum_balance?(balance) && within_promotional_window?(date)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def meets_minimum_balance?(balance)
|
|
126
|
+
balance >= minimum_balance_for_interest
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def minimum_balance_for_interest
|
|
130
|
+
configured = interest_configuration&.minimum_balance_for_interest
|
|
131
|
+
configured.present? ? decimal(configured) : BigDecimal("0")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def interest_rate_tier_for(balance)
|
|
135
|
+
interest_rate_tiers.find do |tier|
|
|
136
|
+
lower_bound_satisfied = balance >= decimal(tier.balance_min)
|
|
137
|
+
upper_bound_satisfied = tier.balance_max.blank? || balance <= decimal(tier.balance_max)
|
|
138
|
+
|
|
139
|
+
lower_bound_satisfied && upper_bound_satisfied
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def interest_rate_tiers
|
|
144
|
+
@interest_rate_tiers ||= begin
|
|
145
|
+
if interest_configuration
|
|
146
|
+
Dscf::Banking::InterestRateTier.by_interest_config(interest_configuration.id).ordered.to_a
|
|
147
|
+
else
|
|
148
|
+
[]
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def within_promotional_window?(date)
|
|
154
|
+
return true unless interest_configuration&.promotional_start_date.present? || interest_configuration&.promotional_end_date.present?
|
|
155
|
+
|
|
156
|
+
date >= interest_configuration.promotional_start_date && date <= interest_configuration.promotional_end_date
|
|
157
|
+
end
|
|
158
|
+
|
|
81
159
|
def normalize_transactions(transactions)
|
|
82
160
|
return [] unless transactions.is_a?(Array)
|
|
83
161
|
|
|
84
|
-
normalized = transactions.map do |transaction|
|
|
162
|
+
normalized = transactions.each_with_index.map do |transaction, index|
|
|
163
|
+
unless transaction.respond_to?(:[]) && (transaction.is_a?(Hash) || transaction.respond_to?(:to_h))
|
|
164
|
+
raise ArgumentError, "Invalid transaction payload at index #{index}"
|
|
165
|
+
end
|
|
166
|
+
|
|
85
167
|
date = to_date(transaction[:date] || transaction["date"])
|
|
86
168
|
amount = decimal(transaction[:amount] || transaction["amount"])
|
|
87
169
|
type = (transaction[:type] || transaction["type"]).to_s.downcase
|
|
@@ -104,7 +186,7 @@ module Dscf::Banking
|
|
|
104
186
|
end
|
|
105
187
|
|
|
106
188
|
def to_date(value)
|
|
107
|
-
Date.
|
|
189
|
+
Date.iso8601(value.to_s)
|
|
108
190
|
rescue Date::Error
|
|
109
191
|
raise ArgumentError, "Invalid date value: #{value}"
|
|
110
192
|
end
|
|
@@ -116,7 +198,7 @@ module Dscf::Banking
|
|
|
116
198
|
end
|
|
117
199
|
|
|
118
200
|
def money(amount)
|
|
119
|
-
decimal(amount).round(2).
|
|
201
|
+
format("%.2f", decimal(amount).round(2).to_f)
|
|
120
202
|
end
|
|
121
203
|
|
|
122
204
|
def accounting_records(gross_interest, tax_amount, net_interest)
|
data/lib/dscf/banking/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dscf-banking
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Asrat Efrem
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-03-
|
|
10
|
+
date: 2026-03-16 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|