dscf-banking 0.5.1 → 0.5.2
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: 7fe37bdff67cefd5cc8827eba966f74185e155c3cf627fed75b4e27ad43ebdec
|
|
4
|
+
data.tar.gz: 905268bd1b0f28a2e806fc7a950851b85915d7bab0354f6bd7473b40b196d3d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8f525a5e131f5fa21f717101a98fd7698477e242818489338fca9b94caa6133bc0feb9d2eea3c2854564449d2454d68a493ca1b7e1797fc6cd64df91b826b5b5
|
|
7
|
+
data.tar.gz: 97c43049bf4d9cbb18cce581dcac6e59b5cc219ac5ae03812edb6875426531a7325765e9ff2ad627bd124cb1786fe94ec06ff5a4442a05dc1ffd58fabf67a8d0
|
|
@@ -274,6 +274,46 @@ module Dscf::Banking
|
|
|
274
274
|
}, status: :not_found
|
|
275
275
|
end
|
|
276
276
|
|
|
277
|
+
def interest_simulations
|
|
278
|
+
account_number = params[:account_number].presence || params[:id]
|
|
279
|
+
account = Dscf::Banking::Account.find_by(account_number: account_number)
|
|
280
|
+
|
|
281
|
+
unless account
|
|
282
|
+
return render json: {
|
|
283
|
+
success: false,
|
|
284
|
+
error: "Account not found"
|
|
285
|
+
}, status: :not_found
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
validation_errors = interest_simulation_validation_errors
|
|
289
|
+
if validation_errors.any?
|
|
290
|
+
return render json: {
|
|
291
|
+
success: false,
|
|
292
|
+
error: "Invalid interest simulation parameters",
|
|
293
|
+
errors: validation_errors
|
|
294
|
+
}, status: :unprocessable_entity
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
simulation = Dscf::Banking::InterestSimulationService.new(
|
|
298
|
+
account: account,
|
|
299
|
+
initial_balance: params[:initial_balance],
|
|
300
|
+
start_date: params[:start_date],
|
|
301
|
+
end_date: params[:end_date],
|
|
302
|
+
transactions: params[:transactions] || []
|
|
303
|
+
).call
|
|
304
|
+
|
|
305
|
+
render json: {
|
|
306
|
+
success: true,
|
|
307
|
+
data: simulation
|
|
308
|
+
}
|
|
309
|
+
rescue ArgumentError => e
|
|
310
|
+
render json: {
|
|
311
|
+
success: false,
|
|
312
|
+
error: "Invalid interest simulation parameters",
|
|
313
|
+
errors: [ e.message ]
|
|
314
|
+
}, status: :unprocessable_entity
|
|
315
|
+
end
|
|
316
|
+
|
|
277
317
|
private
|
|
278
318
|
|
|
279
319
|
def transaction_data(transaction)
|
|
@@ -375,5 +415,18 @@ module Dscf::Banking
|
|
|
375
415
|
update: [ :virtual_account_product, :application ]
|
|
376
416
|
}
|
|
377
417
|
end
|
|
418
|
+
|
|
419
|
+
def interest_simulation_validation_errors
|
|
420
|
+
errors = []
|
|
421
|
+
errors << "initial_balance is required" if params[:initial_balance].blank?
|
|
422
|
+
errors << "start_date is required" if params[:start_date].blank?
|
|
423
|
+
errors << "end_date is required" if params[:end_date].blank?
|
|
424
|
+
|
|
425
|
+
if params[:transactions].present? && !params[:transactions].is_a?(Array)
|
|
426
|
+
errors << "transactions must be an array"
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
errors
|
|
430
|
+
end
|
|
378
431
|
end
|
|
379
432
|
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
require "bigdecimal"
|
|
2
|
+
require "date"
|
|
3
|
+
|
|
4
|
+
module Dscf::Banking
|
|
5
|
+
class InterestSimulationService
|
|
6
|
+
DEFAULT_ANNUAL_INTEREST_RATE = BigDecimal("0.05")
|
|
7
|
+
# Fallback tax rate when no product-level tax is configured.
|
|
8
|
+
DEFAULT_TAX_RATE = BigDecimal("0.15")
|
|
9
|
+
|
|
10
|
+
def initialize(account:, initial_balance:, start_date:, end_date:, transactions: [])
|
|
11
|
+
@account = account
|
|
12
|
+
@initial_balance = decimal(initial_balance)
|
|
13
|
+
@start_date = to_date(start_date)
|
|
14
|
+
@end_date = to_date(end_date)
|
|
15
|
+
@transactions = normalize_transactions(transactions || [])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
raise ArgumentError, "start_date must be before or equal to end_date" if @start_date > @end_date
|
|
20
|
+
|
|
21
|
+
daily_rate = annual_interest_rate / BigDecimal(day_basis.to_s)
|
|
22
|
+
balance = @initial_balance
|
|
23
|
+
gross_interest = BigDecimal("0")
|
|
24
|
+
daily_breakdown = []
|
|
25
|
+
|
|
26
|
+
(@start_date..@end_date).each do |date|
|
|
27
|
+
daily_transactions = @transactions_by_date[date] || []
|
|
28
|
+
daily_transaction_sum = daily_transactions.sum(BigDecimal("0")) { |transaction| transaction[:signed_amount] }
|
|
29
|
+
balance += daily_transaction_sum
|
|
30
|
+
|
|
31
|
+
daily_interest = balance * daily_rate
|
|
32
|
+
gross_interest += daily_interest
|
|
33
|
+
|
|
34
|
+
daily_breakdown << {
|
|
35
|
+
date: date.iso8601,
|
|
36
|
+
balance: money(balance),
|
|
37
|
+
transaction_total: money(daily_transaction_sum),
|
|
38
|
+
daily_interest: money(daily_interest)
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
tax_amount = gross_interest * tax_rate
|
|
43
|
+
net_interest = gross_interest - tax_amount
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
gross_interest: money(gross_interest),
|
|
47
|
+
tax_amount: money(tax_amount),
|
|
48
|
+
net_interest: money(net_interest),
|
|
49
|
+
daily_breakdown: daily_breakdown,
|
|
50
|
+
accounting_records: accounting_records(gross_interest, tax_amount, net_interest)
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def interest_configuration
|
|
57
|
+
@interest_configuration ||= Dscf::Banking::InterestConfiguration
|
|
58
|
+
.active
|
|
59
|
+
.find_by(virtual_account_product_id: @account.virtual_account_product_id)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def annual_interest_rate
|
|
63
|
+
configured = interest_configuration&.annual_interest_rate
|
|
64
|
+
configured.present? ? decimal(configured) : DEFAULT_ANNUAL_INTEREST_RATE
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def tax_rate
|
|
68
|
+
configured = interest_configuration&.income_tax_rate
|
|
69
|
+
configured.present? ? decimal(configured) : DEFAULT_TAX_RATE
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def day_basis
|
|
73
|
+
case interest_configuration&.interest_basis
|
|
74
|
+
when "actual_360", "thirty_360"
|
|
75
|
+
360
|
|
76
|
+
else
|
|
77
|
+
365
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def normalize_transactions(transactions)
|
|
82
|
+
return [] unless transactions.is_a?(Array)
|
|
83
|
+
|
|
84
|
+
normalized = transactions.map do |transaction|
|
|
85
|
+
date = to_date(transaction[:date] || transaction["date"])
|
|
86
|
+
amount = decimal(transaction[:amount] || transaction["amount"])
|
|
87
|
+
type = (transaction[:type] || transaction["type"]).to_s.downcase
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
date: date,
|
|
91
|
+
signed_amount: signed_amount(amount, type)
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
@transactions_by_date = normalized.group_by { |transaction| transaction[:date] }
|
|
96
|
+
normalized
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def signed_amount(amount, type)
|
|
100
|
+
return amount * -1 if %w[debit withdrawal outflow].include?(type) && amount.positive?
|
|
101
|
+
return amount.abs if %w[credit deposit inflow].include?(type)
|
|
102
|
+
|
|
103
|
+
amount
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def to_date(value)
|
|
107
|
+
Date.parse(value.to_s)
|
|
108
|
+
rescue Date::Error
|
|
109
|
+
raise ArgumentError, "Invalid date value: #{value}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def decimal(value)
|
|
113
|
+
BigDecimal(value.to_s)
|
|
114
|
+
rescue ArgumentError
|
|
115
|
+
raise ArgumentError, "Invalid numeric value: #{value}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def money(amount)
|
|
119
|
+
decimal(amount).round(2).to_s("F")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def accounting_records(gross_interest, tax_amount, net_interest)
|
|
123
|
+
[
|
|
124
|
+
{
|
|
125
|
+
date: @end_date.iso8601,
|
|
126
|
+
type: "gross_interest",
|
|
127
|
+
amount: money(gross_interest),
|
|
128
|
+
currency: @account.currency
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
date: @end_date.iso8601,
|
|
132
|
+
type: "tax",
|
|
133
|
+
amount: money(tax_amount),
|
|
134
|
+
currency: @account.currency
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
date: @end_date.iso8601,
|
|
138
|
+
type: "net_interest",
|
|
139
|
+
amount: money(net_interest),
|
|
140
|
+
currency: @account.currency
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
data/config/routes.rb
CHANGED
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.2
|
|
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-15 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|
|
@@ -472,6 +472,7 @@ files:
|
|
|
472
472
|
- app/services/dscf/banking/account_creation_service.rb
|
|
473
473
|
- app/services/dscf/banking/base_transaction_service.rb
|
|
474
474
|
- app/services/dscf/banking/deposit_service.rb
|
|
475
|
+
- app/services/dscf/banking/interest_simulation_service.rb
|
|
475
476
|
- app/services/dscf/banking/transfer_service.rb
|
|
476
477
|
- app/services/dscf/banking/voucher_service.rb
|
|
477
478
|
- app/services/dscf/banking/withdrawal_service.rb
|